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 "MCPAssetFinder.h"
#include "Engine/Engine.h"
#include "Engine/Blueprint.h" #include "Engine/Blueprint.h"
#include "Engine/World.h" #include "Engine/World.h"
#include "Engine/Level.h" #include "Engine/Level.h"
#include "Engine/LevelScriptBlueprint.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 "MCPUtils.h"
#include "AssetRegistry/AssetRegistryModule.h" #include "AssetRegistry/AssetRegistryModule.h"
#include "AssetRegistry/IAssetRegistry.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 // MCPAssetsBase
// ============================================================ // ============================================================
@@ -263,7 +27,7 @@ MCPAssetsBase& MCPAssetsBase::Exact(const FString& InName)
MCPAssetsBase& MCPAssetsBase::Substring(const FString& InFilter) MCPAssetsBase& MCPAssetsBase::Substring(const FString& InFilter)
{ {
MatchName = InFilter; MatchName = InFilter;
bExactMatch = false; bExactMatch = false;
bPatternHasSlash = MatchName.Contains(TEXT("/")); bPatternHasSlash = MatchName.Contains(TEXT("/"));
return *this; return *this;
} }
@@ -411,20 +175,3 @@ void MCPAssetsBase::SetError(const FString &Msg)
UObjectResults.Empty(); UObjectResults.Empty();
ErrorCB.SetError(Msg); 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); bool bSaved = MCPUtils::SaveBlueprintPackage(NewAnimBP);
// Collect graph names TArray<TSharedPtr<FJsonValue>> GraphNames = MCPUtils::AllGraphNamesJson(NewAnimBP);
TArray<TSharedPtr<FJsonValue>> GraphNames;
TArray<UEdGraph*> AllGraphs;
NewAnimBP->GetAllGraphs(AllGraphs);
for (UEdGraph* Graph : AllGraphs)
{
if (Graph)
{
GraphNames.Add(MakeShared<FJsonValueString>(Graph->GetName()));
}
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created AnimBlueprint '%s' with %d graphs (saved: %s)"), UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created AnimBlueprint '%s' with %d graphs (saved: %s)"),
*Name, GraphNames.Num(), bSaved ? TEXT("true") : TEXT("false")); *Name, GraphNames.Num(), bSaved ? TEXT("true") : TEXT("false"));
@@ -172,25 +162,17 @@ public:
// Walk all anim nodes to collect slot names // Walk all anim nodes to collect slot names
TSet<FString> SlotNames; TSet<FString> SlotNames;
TArray<UEdGraph*> AllGraphs; for (UAnimGraphNode_Base* AnimNode : MCPUtils::AllNodes<UAnimGraphNode_Base>(AnimBP))
AnimBP->GetAllGraphs(AllGraphs);
for (UEdGraph* Graph : AllGraphs)
{ {
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 FName SlotValue = PropIt->GetPropertyValue_InContainer(AnimNode);
for (TFieldIterator<FNameProperty> PropIt(AnimNode->GetClass()); PropIt; ++PropIt) if (!SlotValue.IsNone())
{ {
if (PropIt->GetName().Contains(TEXT("SlotName")) || PropIt->GetName().Contains(TEXT("Slot"))) SlotNames.Add(SlotValue.ToString());
{
FName SlotValue = PropIt->GetPropertyValue_InContainer(AnimNode);
if (!SlotValue.IsNone())
{
SlotNames.Add(SlotValue.ToString());
}
}
} }
} }
} }
@@ -238,25 +220,17 @@ public:
// Walk all anim nodes to collect sync group names // Walk all anim nodes to collect sync group names
TSet<FString> SyncGroupNames; TSet<FString> SyncGroupNames;
TArray<UEdGraph*> AllGraphs; for (UAnimGraphNode_Base* AnimNode : MCPUtils::AllNodes<UAnimGraphNode_Base>(AnimBP))
AnimBP->GetAllGraphs(AllGraphs);
for (UEdGraph* Graph : AllGraphs)
{ {
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 FName GroupValue = PropIt->GetPropertyValue_InContainer(AnimNode);
for (TFieldIterator<FNameProperty> PropIt(AnimNode->GetClass()); PropIt; ++PropIt) if (!GroupValue.IsNone())
{ {
if (PropIt->GetName().Contains(TEXT("SyncGroup")) || PropIt->GetName().Contains(TEXT("GroupName"))) SyncGroupNames.Add(GroupValue.ToString());
{
FName GroupValue = PropIt->GetPropertyValue_InContainer(AnimNode);
if (!GroupValue.IsNone())
{
SyncGroupNames.Add(GroupValue.ToString());
}
}
} }
} }
} }

View File

@@ -57,16 +57,11 @@ public:
} }
// Check against existing graphs (functions, macros, etc.) // Check against existing graphs (functions, macros, etc.)
TArray<UEdGraph*> AllGraphs; if (!MCPUtils::AllGraphsNamed(BP, DispatcherName).IsEmpty())
BP->GetAllGraphs(AllGraphs);
for (UEdGraph* Existing : AllGraphs)
{ {
if (Existing && Existing->GetName().Equals(DispatcherName, ESearchCase::IgnoreCase)) return MCPUtils::MakeErrorJson(Result, FString::Printf(
{ TEXT("A graph named '%s' already exists in Blueprint '%s'"),
return MCPUtils::MakeErrorJson(Result, FString::Printf( *DispatcherName, *Blueprint));
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'"), 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 // Find the entry node in the signature graph
UK2Node_EditablePinBase* EntryNode = nullptr; 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) if (!EntryNode)
@@ -197,11 +189,8 @@ public:
UEdGraph* SigGraph = FBlueprintEditorUtils::GetDelegateSignatureGraphByName(BP, DelegateName); UEdGraph* SigGraph = FBlueprintEditorUtils::GetDelegateSignatureGraphByName(BP, DelegateName);
if (SigGraph) 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) for (const TSharedPtr<FUserPinInfo>& PinInfo : FE->UserDefinedPins)
{ {
if (!PinInfo.IsValid()) continue; if (!PinInfo.IsValid()) continue;

View File

@@ -248,19 +248,7 @@ public:
// Collect graph names // Collect graph names
TArray<TSharedPtr<FJsonValue>> GraphNames; TArray<TSharedPtr<FJsonValue>> GraphNames = MCPUtils::AllGraphNamesJson(NewBP);
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()));
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Blueprint '%s' with %d graphs (saved: %s)"), UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Blueprint '%s' with %d graphs (saved: %s)"),
*Blueprint, GraphNames.Num(), bSaved ? TEXT("true") : TEXT("false")); *Blueprint, GraphNames.Num(), bSaved ? TEXT("true") : TEXT("false"));
@@ -310,33 +298,21 @@ public:
UBlueprint* BP = Assets.Object(); UBlueprint* BP = Assets.Object();
// Check graph name uniqueness // Check graph name uniqueness
TArray<UEdGraph*> AllGraphs; if (!MCPUtils::AllGraphsNamed(BP, Graph).IsEmpty())
BP->GetAllGraphs(AllGraphs);
for (UEdGraph* Existing : AllGraphs)
{ {
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 // Also check for existing custom events with the same name
if (GraphType == TEXT("customEvent")) if (GraphType == TEXT("customEvent"))
{ {
for (UEdGraph* ExistingGraph : AllGraphs) for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
{ {
if (!ExistingGraph) continue; if (CE->CustomFunctionName == FName(*Graph))
for (UEdGraphNode* Node : ExistingGraph->Nodes)
{ {
if (UK2Node_CustomEvent* CE = Cast<UK2Node_CustomEvent>(Node)) return MCPUtils::MakeErrorJson(Result, FString::Printf(
{ TEXT("A custom event named '%s' already exists in Blueprint '%s'"), *Graph, *Blueprint));
if (CE->CustomFunctionName == FName(*Graph))
{
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 // Check for name collision
TArray<UEdGraph*> AllGraphs; for (UEdGraph* Existing : MCPUtils::AllGraphsNamed(BP, NewName))
BP->GetAllGraphs(AllGraphs);
for (UEdGraph* Existing : AllGraphs)
{ {
if (Existing && Existing != TargetGraph && Existing->GetName().Equals(NewName, ESearchCase::IgnoreCase)) if (Existing != TargetGraph)
{ {
return MCPUtils::MakeErrorJson(Result, FString::Printf( return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("A graph named '%s' already exists in Blueprint '%s'"), *NewName, *Blueprint)); TEXT("A graph named '%s' already exists in Blueprint '%s'"), *NewName, *Blueprint));

View File

@@ -180,23 +180,12 @@ public:
// Find the target graph // Find the target graph
FString DecodedGraphName = MCPUtils::UrlDecode(Graph); FString DecodedGraphName = MCPUtils::UrlDecode(Graph);
UEdGraph* TargetGraph = nullptr; TArray<UEdGraph*> MatchingGraphs = MCPUtils::AllGraphsNamed(BP, DecodedGraphName);
TArray<UEdGraph*> AllGraphs; if (MatchingGraphs.Num() == 0)
BP->GetAllGraphs(AllGraphs);
for (UEdGraph* G : AllGraphs)
{
if (G && G->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase))
{
TargetGraph = G;
break;
}
}
if (!TargetGraph)
{ {
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName)); return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName));
} }
UEdGraph* TargetGraph = MatchingGraphs[0];
if (Nodes.Array.Num() == 0) if (Nodes.Array.Num() == 0)
{ {
@@ -350,30 +339,19 @@ public:
// Find the target graph // Find the target graph
FString DecodedGraphName = MCPUtils::UrlDecode(Graph); FString DecodedGraphName = MCPUtils::UrlDecode(Graph);
UEdGraph* TargetGraph = nullptr; TArray<UEdGraph*> MatchingGraphs = MCPUtils::AllGraphsNamed(BP, DecodedGraphName);
TArray<UEdGraph*> AllGraphs; if (MatchingGraphs.Num() == 0)
BP->GetAllGraphs(AllGraphs);
for (UEdGraph* G : AllGraphs)
{
if (G && G->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase))
{
TargetGraph = G;
break;
}
}
if (!TargetGraph)
{ {
TArray<TSharedPtr<FJsonValue>> GraphNames; 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)); MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName));
Result->SetArrayField(TEXT("availableGraphs"), GraphNames); Result->SetArrayField(TEXT("availableGraphs"), GraphNames);
return; return;
} }
UEdGraph* TargetGraph = MatchingGraphs[0];
TArray<TSharedPtr<FJsonValue>> Results; TArray<TSharedPtr<FJsonValue>> Results;
int32 SuccessCount = 0; int32 SuccessCount = 0;
@@ -713,13 +691,10 @@ public:
*Blueprint, *OldClass, *NewClass, *NewClassPtr->GetPathName()); *Blueprint, *OldClass, *NewClass, *NewClassPtr->GetPathName());
// Find all CallFunction nodes // Find all CallFunction nodes
TArray<UK2Node_CallFunction*> AllCallNodes;
FBlueprintEditorUtils::GetAllNodesOfClass<UK2Node_CallFunction>(BP, AllCallNodes);
int32 ReplacedCount = 0; int32 ReplacedCount = 0;
TArray<TSharedPtr<FJsonValue>> BrokenConnections; TArray<TSharedPtr<FJsonValue>> BrokenConnections;
for (UK2Node_CallFunction* CallNode : AllCallNodes) for (UK2Node_CallFunction* CallNode : MCPUtils::AllNodes<UK2Node_CallFunction>(BP))
{ {
UClass* ParentClass = CallNode->FunctionReference.GetMemberParentClass(); UClass* ParentClass = CallNode->FunctionReference.GetMemberParentClass();
if (!ParentClass) if (!ParentClass)
@@ -920,14 +895,8 @@ public:
UBlueprint* BP = Assets.Object(); UBlueprint* BP = Assets.Object();
// Count graphs and nodes before refresh // Count graphs and nodes before refresh
TArray<UEdGraph*> AllGraphs; int32 GraphCount = MCPUtils::AllGraphs(BP).Num();
BP->GetAllGraphs(AllGraphs); int32 NodeCount = MCPUtils::AllNodes(BP).Num();
int32 GraphCount = AllGraphs.Num();
int32 NodeCount = 0;
for (UEdGraph* G : AllGraphs)
{
if (G) NodeCount += G->Nodes.Num();
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Refreshing all nodes in '%s' (%d graphs, %d nodes)"), UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Refreshing all nodes in '%s' (%d graphs, %d nodes)"),
*Blueprint, GraphCount, NodeCount); *Blueprint, GraphCount, NodeCount);
@@ -937,21 +906,16 @@ public:
// Remove orphaned pins from all nodes // Remove orphaned pins from all nodes
int32 OrphanedPinsRemoved = 0; int32 OrphanedPinsRemoved = 0;
for (UEdGraph* G : AllGraphs) for (UEdGraphNode* Node : MCPUtils::AllNodes(BP))
{ {
if (!G) continue; for (int32 i = Node->Pins.Num() - 1; i >= 0; --i)
for (UEdGraphNode* Node : G->Nodes)
{ {
if (!Node) continue; UEdGraphPin* Pin = Node->Pins[i];
for (int32 i = Node->Pins.Num() - 1; i >= 0; --i) if (Pin && Pin->bOrphanedPin)
{ {
UEdGraphPin* Pin = Node->Pins[i]; Pin->BreakAllPinLinks();
if (Pin && Pin->bOrphanedPin) 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 // Check each graph for nodes with error/warning status
AllGraphs.Empty(); for (UEdGraphNode* Node : MCPUtils::AllNodes(BP))
BP->GetAllGraphs(AllGraphs);
for (UEdGraph* G : AllGraphs)
{ {
if (!G) continue; if (Node->bHasCompilerMessage)
for (UEdGraphNode* Node : G->Nodes)
{ {
if (!Node) continue; FString NodeTitle = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
if (Node->bHasCompilerMessage) 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(); ErrorsArr.Add(MakeShared<FJsonValueString>(NodeMsg));
FString NodeMsg = FString::Printf(TEXT("[%s] %s: %s"), }
*G->GetName(), *NodeTitle, *Node->ErrorMsg); else
if (Node->ErrorType == EMessageSeverity::Error) {
{ 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(); UBlueprint* BP = BPAssets.Object();
FString DecodedGraphName = MCPUtils::UrlDecode(Graph); FString DecodedGraphName = MCPUtils::UrlDecode(Graph);
TArray<UEdGraph*> AllGraphs; TArray<UEdGraph*> MatchingGraphs = MCPUtils::AllGraphsNamed(BP, DecodedGraphName);
BP->GetAllGraphs(AllGraphs); if (MatchingGraphs.Num() > 0)
for (UEdGraph* G : AllGraphs)
{ {
if (G && G->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase)) GraphFilter = MatchingGraphs[0];
{
GraphFilter = G;
break;
}
} }
if (!GraphFilter) if (!GraphFilter)
{ {

View File

@@ -59,46 +59,28 @@ public:
UK2Node_EditablePinBase* EntryNode = nullptr; UK2Node_EditablePinBase* EntryNode = nullptr;
FString FoundNodeType; FString FoundNodeType;
TArray<UEdGraph*> AllGraphs; // Strategy 1: Look for a K2Node_FunctionEntry in a function graph matching the name
BP->GetAllGraphs(AllGraphs); for (UK2Node_FunctionEntry* FuncEntry : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
// Strategy 1: Look for a function graph matching the name
for (UEdGraph* Graph : AllGraphs)
{ {
if (Graph && Graph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) if (FuncEntry->GetGraph()->GetName().Equals(FunctionName, ESearchCase::IgnoreCase))
{ {
for (UEdGraphNode* Node : Graph->Nodes) EntryNode = FuncEntry;
{ FoundNodeType = TEXT("FunctionEntry");
if (UK2Node_FunctionEntry* FuncEntry = Cast<UK2Node_FunctionEntry>(Node)) break;
{
EntryNode = FuncEntry;
FoundNodeType = TEXT("FunctionEntry");
break;
}
}
if (EntryNode) break;
} }
} }
// Strategy 2: Search for a K2Node_CustomEvent with matching CustomFunctionName // Strategy 2: Search for a K2Node_CustomEvent with matching CustomFunctionName
if (!EntryNode) if (!EntryNode)
{ {
for (UEdGraph* Graph : AllGraphs) for (UK2Node_CustomEvent* CustomEvent : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
{ {
if (!Graph) continue; if (CustomEvent->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase))
for (UEdGraphNode* Node : Graph->Nodes)
{ {
if (UK2Node_CustomEvent* CustomEvent = Cast<UK2Node_CustomEvent>(Node)) EntryNode = CustomEvent;
{ FoundNodeType = TEXT("CustomEvent");
if (CustomEvent->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase)) break;
{
EntryNode = CustomEvent;
FoundNodeType = TEXT("CustomEvent");
break;
}
}
} }
if (EntryNode) break;
} }
} }
@@ -106,22 +88,15 @@ public:
{ {
// List available functions/events for debugging // List available functions/events for debugging
TArray<TSharedPtr<FJsonValue>> Available; TArray<TSharedPtr<FJsonValue>> Available;
for (UEdGraph* Graph : AllGraphs) for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
{ {
if (!Graph) continue; Available.Add(MakeShared<FJsonValueString>(
for (UEdGraphNode* Node : Graph->Nodes) FString::Printf(TEXT("function:%s"), *FE->GetGraph()->GetName())));
{ }
if (UK2Node_FunctionEntry* FE = Cast<UK2Node_FunctionEntry>(Node)) for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
{ {
Available.Add(MakeShared<FJsonValueString>( Available.Add(MakeShared<FJsonValueString>(
FString::Printf(TEXT("function:%s"), *Graph->GetName()))); FString::Printf(TEXT("event:%s"), *CE->CustomFunctionName.ToString())));
}
else if (UK2Node_CustomEvent* CE = Cast<UK2Node_CustomEvent>(Node))
{
Available.Add(MakeShared<FJsonValueString>(
FString::Printf(TEXT("event:%s"), *CE->CustomFunctionName.ToString())));
}
}
} }
MCPUtils::MakeErrorJson(Result, FString::Printf( MCPUtils::MakeErrorJson(Result, FString::Printf(
@@ -261,46 +236,28 @@ public:
UK2Node_EditablePinBase* EntryNode = nullptr; UK2Node_EditablePinBase* EntryNode = nullptr;
FString FoundNodeType; FString FoundNodeType;
TArray<UEdGraph*> AllGraphs; // Strategy 1: Look for a K2Node_FunctionEntry in a function graph matching the name
BP->GetAllGraphs(AllGraphs); for (UK2Node_FunctionEntry* FuncEntry : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
// Strategy 1: Look for a function graph matching the name
for (UEdGraph* Graph : AllGraphs)
{ {
if (Graph && Graph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) if (FuncEntry->GetGraph()->GetName().Equals(FunctionName, ESearchCase::IgnoreCase))
{ {
for (UEdGraphNode* Node : Graph->Nodes) EntryNode = FuncEntry;
{ FoundNodeType = TEXT("FunctionEntry");
if (UK2Node_FunctionEntry* FuncEntry = Cast<UK2Node_FunctionEntry>(Node)) break;
{
EntryNode = FuncEntry;
FoundNodeType = TEXT("FunctionEntry");
break;
}
}
if (EntryNode) break;
} }
} }
// Strategy 2: Search for a K2Node_CustomEvent with matching CustomFunctionName // Strategy 2: Search for a K2Node_CustomEvent with matching CustomFunctionName
if (!EntryNode) if (!EntryNode)
{ {
for (UEdGraph* Graph : AllGraphs) for (UK2Node_CustomEvent* CustomEvent : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
{ {
if (!Graph) continue; if (CustomEvent->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase))
for (UEdGraphNode* Node : Graph->Nodes)
{ {
if (UK2Node_CustomEvent* CustomEvent = Cast<UK2Node_CustomEvent>(Node)) EntryNode = CustomEvent;
{ FoundNodeType = TEXT("CustomEvent");
if (CustomEvent->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase)) break;
{
EntryNode = CustomEvent;
FoundNodeType = TEXT("CustomEvent");
break;
}
}
} }
if (EntryNode) break;
} }
} }
@@ -308,22 +265,15 @@ public:
{ {
// List available functions/events for debugging // List available functions/events for debugging
TArray<TSharedPtr<FJsonValue>> Available; TArray<TSharedPtr<FJsonValue>> Available;
for (UEdGraph* Graph : AllGraphs) for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
{ {
if (!Graph) continue; Available.Add(MakeShared<FJsonValueString>(
for (UEdGraphNode* Node : Graph->Nodes) FString::Printf(TEXT("function:%s"), *FE->GetGraph()->GetName())));
{ }
if (UK2Node_FunctionEntry* FE = Cast<UK2Node_FunctionEntry>(Node)) for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
{ {
Available.Add(MakeShared<FJsonValueString>( Available.Add(MakeShared<FJsonValueString>(
FString::Printf(TEXT("function:%s"), *Graph->GetName()))); FString::Printf(TEXT("event:%s"), *CE->CustomFunctionName.ToString())));
}
else if (UK2Node_CustomEvent* CE = Cast<UK2Node_CustomEvent>(Node))
{
Available.Add(MakeShared<FJsonValueString>(
FString::Printf(TEXT("event:%s"), *CE->CustomFunctionName.ToString())));
}
}
} }
MCPUtils::MakeErrorJson(Result, FString::Printf( MCPUtils::MakeErrorJson(Result, FString::Printf(
@@ -436,75 +386,44 @@ public:
FName FuncFName(*FunctionName); FName FuncFName(*FunctionName);
// Strategy 1: K2Node_FunctionEntry in function graphs // Strategy 1: K2Node_FunctionEntry in function graphs
TArray<UEdGraph*> AllGraphs; for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
BP->GetAllGraphs(AllGraphs);
for (UEdGraph* Graph : AllGraphs)
{ {
if (!Graph || !Graph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) UEdGraph* FEGraph = FE->GetGraph();
{ if (!FEGraph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) continue;
continue;
}
// Skip delegate signature graphs (handled in Strategy 3) // Skip delegate signature graphs (handled in Strategy 3)
if (BP->DelegateSignatureGraphs.Contains(Graph)) if (BP->DelegateSignatureGraphs.Contains(FEGraph)) continue;
{
continue;
}
for (UEdGraphNode* Node : Graph->Nodes) EntryNode = FE;
{ NodeType = TEXT("FunctionEntry");
if (UK2Node_FunctionEntry* FE = Cast<UK2Node_FunctionEntry>(Node)) break;
{
EntryNode = FE;
NodeType = TEXT("FunctionEntry");
break;
}
}
if (EntryNode) break;
} }
// Strategy 2: K2Node_CustomEvent with matching CustomFunctionName // Strategy 2: K2Node_CustomEvent with matching CustomFunctionName
if (!EntryNode) if (!EntryNode)
{ {
for (UEdGraph* Graph : AllGraphs) for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
{ {
if (!Graph) continue; if (CE->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase))
for (UEdGraphNode* Node : Graph->Nodes)
{ {
if (UK2Node_CustomEvent* CE = Cast<UK2Node_CustomEvent>(Node)) EntryNode = CE;
{ NodeType = TEXT("CustomEvent");
if (CE->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase)) break;
{
EntryNode = CE;
NodeType = TEXT("CustomEvent");
break;
}
}
} }
if (EntryNode) break;
} }
} }
// Strategy 3: K2Node_FunctionEntry in DelegateSignatureGraphs // Strategy 3: K2Node_FunctionEntry in DelegateSignatureGraphs
if (!EntryNode) if (!EntryNode)
{ {
for (UEdGraph* SigGraph : BP->DelegateSignatureGraphs) for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
{ {
if (!SigGraph || !SigGraph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) UEdGraph* FEGraph = FE->GetGraph();
{ if (!FEGraph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) continue;
continue; if (!BP->DelegateSignatureGraphs.Contains(FEGraph)) continue;
}
for (UEdGraphNode* Node : SigGraph->Nodes) EntryNode = FE;
{ NodeType = TEXT("EventDispatcher");
if (UK2Node_FunctionEntry* FE = Cast<UK2Node_FunctionEntry>(Node)) break;
{
EntryNode = FE;
NodeType = TEXT("EventDispatcher");
break;
}
}
if (EntryNode) break;
} }
} }
@@ -519,17 +438,10 @@ public:
} }
// Custom events // Custom events
for (UEdGraph* Graph : AllGraphs) for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
{ {
if (!Graph) continue; AvailFuncs.Add(MakeShared<FJsonValueString>(
for (UEdGraphNode* Node : Graph->Nodes) FString::Printf(TEXT("%s (custom event)"), *CE->CustomFunctionName.ToString())));
{
if (UK2Node_CustomEvent* CE = Cast<UK2Node_CustomEvent>(Node))
{
AvailFuncs.Add(MakeShared<FJsonValueString>(
FString::Printf(TEXT("%s (custom event)"), *CE->CustomFunctionName.ToString())));
}
}
} }
// Dispatchers // Dispatchers

View File

@@ -194,12 +194,11 @@ public:
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return; if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
UBlueprint* BP = Assets.Object(); UBlueprint* BP = Assets.Object();
TArray<UEdGraph*> AllGraphs; TArray<UEdGraph*> AllGraphs = MCPUtils::AllGraphs(BP);
BP->GetAllGraphs(AllGraphs);
for (UEdGraph* GraphObj : AllGraphs) 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); TSharedPtr<FJsonObject> GraphJson = MCPUtils::SerializeGraph(GraphObj);
if (GraphJson.IsValid()) if (GraphJson.IsValid())
@@ -214,10 +213,7 @@ public:
TArray<TSharedPtr<FJsonValue>> GraphNames; TArray<TSharedPtr<FJsonValue>> GraphNames;
for (UEdGraph* GraphObj : AllGraphs) 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)); MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName));
Result->SetArrayField(TEXT("availableGraphs"), GraphNames); Result->SetArrayField(TEXT("availableGraphs"), GraphNames);
@@ -255,59 +251,51 @@ public:
// Build a combined list of all searchable blueprints (regular + level) // Build a combined list of all searchable blueprints (regular + level)
auto SearchBlueprint = [&](const FString& AssetName, const FString& AssetPath, UBlueprint* BP, TArray<TSharedPtr<FJsonValue>>& OutResults) auto SearchBlueprint = [&](const FString& AssetName, const FString& AssetPath, UBlueprint* BP, TArray<TSharedPtr<FJsonValue>>& OutResults)
{ {
TArray<UEdGraph*> Graphs; for (UEdGraphNode* Node : MCPUtils::AllNodes(BP))
BP->GetAllGraphs(Graphs);
for (UEdGraph* GraphObj : Graphs)
{ {
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 (bMatch)
if (auto* CF = Cast<UK2Node_CallFunction>(Node)) {
{ TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
FuncName = CF->FunctionReference.GetMemberName().ToString(); R->SetStringField(TEXT("blueprint"), AssetName);
} R->SetStringField(TEXT("blueprintPath"), AssetPath);
else if (auto* Ev = Cast<UK2Node_Event>(Node)) R->SetStringField(TEXT("graph"), Node->GetGraph()->GetName());
{ R->SetStringField(TEXT("nodeTitle"), Title);
EventName = Ev->EventReference.GetMemberName().ToString(); R->SetStringField(TEXT("nodeClass"), Node->GetClass()->GetName());
} if (!FuncName.IsEmpty()) R->SetStringField(TEXT("functionName"), FuncName);
else if (auto* CE = Cast<UK2Node_CustomEvent>(Node)) if (!EventName.IsEmpty()) R->SetStringField(TEXT("eventName"), EventName);
{ if (!VarName.IsEmpty()) R->SetStringField(TEXT("variableName"), VarName);
EventName = CE->CustomFunctionName.ToString(); OutResults.Add(MakeShared<FJsonValueObject>(R));
}
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));
}
} }
} }
}; };
@@ -549,137 +537,129 @@ public:
} }
// Check graphs for function/event params, struct nodes, and pin connections // Check graphs for function/event params, struct nodes, and pin connections
TArray<UEdGraph*> AllGraphs; for (UEdGraphNode* Node : MCPUtils::AllNodes(BP))
BP->GetAllGraphs(AllGraphs);
for (UEdGraph* GraphObj : AllGraphs)
{ {
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; for (const TSharedPtr<FUserPinInfo>& PinInfo : FuncEntry->UserDefinedPins)
// Check FunctionEntry/CustomEvent parameters
if (auto* FuncEntry = Cast<UK2Node_FunctionEntry>(Node))
{ {
for (const TSharedPtr<FUserPinInfo>& PinInfo : FuncEntry->UserDefinedPins) if (!PinInfo.IsValid()) continue;
{ FString ParamSubtype;
if (!PinInfo.IsValid()) continue; if (PinInfo->PinType.PinSubCategoryObject.IsValid())
FString ParamSubtype; ParamSubtype = PinInfo->PinType.PinSubCategoryObject->GetName();
if (PinInfo->PinType.PinSubCategoryObject.IsValid())
ParamSubtype = PinInfo->PinType.PinSubCategoryObject->GetName();
if (MatchesType(ParamSubtype) || MatchesType(PinInfo->PinType.PinCategory.ToString())) 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()))
{ {
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>(); TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
R->SetStringField(TEXT("blueprint"), BPName); R->SetStringField(TEXT("blueprint"), BPName);
R->SetStringField(TEXT("blueprintPath"), BPPath); R->SetStringField(TEXT("blueprintPath"), BPPath);
R->SetStringField(TEXT("usage"), TEXT("breakStruct")); R->SetStringField(TEXT("usage"), TEXT("functionParameter"));
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("location"), FString::Printf(TEXT("%s.%s"), R->SetStringField(TEXT("location"), FString::Printf(TEXT("%s.%s"),
*Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString(), *Node->GetGraph()->GetName(), *PinInfo->PinName.ToString()));
*Pin->PinName.ToString()));
R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString()); R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
R->SetStringField(TEXT("graph"), GraphObj->GetName()); R->SetStringField(TEXT("currentType"), PinInfo->PinType.PinCategory.ToString());
R->SetStringField(TEXT("pinType"), Pin->PinType.PinCategory.ToString()); if (!ParamSubtype.IsEmpty())
if (!PinSubtype.IsEmpty()) R->SetStringField(TEXT("currentSubtype"), ParamSubtype);
R->SetStringField(TEXT("pinSubtype"), PinSubtype);
R->SetNumberField(TEXT("connectionCount"), Pin->LinkedTo.Num());
if (bIsLevel) if (bIsLevel)
R->SetBoolField(TEXT("isLevelBlueprint"), true); R->SetBoolField(TEXT("isLevelBlueprint"), true);
Results.Add(MakeShared<FJsonValueObject>(R)); 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>> ErrorsArr;
TArray<TSharedPtr<FJsonValue>> WarningsArr; TArray<TSharedPtr<FJsonValue>> WarningsArr;
TArray<UEdGraph*> AllGraphs; for (UEdGraphNode* Node : MCPUtils::AllNodes(BP))
BP->GetAllGraphs(AllGraphs);
for (UEdGraph* Graph : AllGraphs)
{ {
if (!Graph) continue; if (Node->bHasCompilerMessage)
for (UEdGraphNode* Node : Graph->Nodes)
{ {
if (!Node) continue; TSharedRef<FJsonObject> Msg = MakeShared<FJsonObject>();
if (Node->bHasCompilerMessage) Msg->SetStringField(TEXT("graph"), Node->GetGraph()->GetName());
{ Msg->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
TSharedRef<FJsonObject> Msg = MakeShared<FJsonObject>(); Msg->SetStringField(TEXT("nodeTitle"), Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString());
Msg->SetStringField(TEXT("graph"), Graph->GetName()); Msg->SetStringField(TEXT("nodeClass"), Node->GetClass()->GetName());
Msg->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString()); Msg->SetStringField(TEXT("message"), Node->ErrorMsg);
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) if (Node->ErrorType == EMessageSeverity::Error)
{ {
Msg->SetStringField(TEXT("severity"), TEXT("error")); Msg->SetStringField(TEXT("severity"), TEXT("error"));
ErrorsArr.Add(MakeShared<FJsonValueObject>(Msg)); ErrorsArr.Add(MakeShared<FJsonValueObject>(Msg));
} }
else else
{ {
Msg->SetStringField(TEXT("severity"), TEXT("warning")); Msg->SetStringField(TEXT("severity"), TEXT("warning"));
WarningsArr.Add(MakeShared<FJsonValueObject>(Msg)); WarningsArr.Add(MakeShared<FJsonValueObject>(Msg));
}
} }
} }
} }
@@ -121,7 +113,7 @@ public:
default: StatusStr = FString::Printf(TEXT("Status_%d"), (int32)BP->Status); break; 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>(); TSharedRef<FJsonObject> Result = MakeShared<FJsonObject>();
Result->SetStringField(TEXT("blueprint"), BlueprintName); Result->SetStringField(TEXT("blueprint"), BlueprintName);

View File

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

View File

@@ -251,7 +251,6 @@ int32 TryAddMaterialExpressionSEH(
void FMCPServer::DispatchToolCall(const FString& ToolName, const FJsonObject* Params, FJsonObject* Result) void FMCPServer::DispatchToolCall(const FString& ToolName, const FJsonObject* Params, FJsonObject* Result)
{ {
UMCPAssetFinder::Refresh();
if (UClass** HandlerClass = MCPHandlerRegistry.Find(ToolName)) if (UClass** HandlerClass = MCPHandlerRegistry.Find(ToolName))
{ {
const bool bIsMutation = MutationEndpoints.Contains(ToolName); const bool bIsMutation = MutationEndpoints.Contains(ToolName);

View File

@@ -189,6 +189,38 @@ FString MCPUtils::UrlDecode(const FString& EncodedString)
// Blueprint helpers // 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( UEdGraphNode* MCPUtils::FindNodeByGuid(
UBlueprint* BP, const FString& GuidString, UEdGraph** OutGraph) UBlueprint* BP, const FString& GuidString, UEdGraph** OutGraph)
{ {

View File

@@ -1,112 +1,14 @@
#pragma once #pragma once
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "Subsystems/EngineSubsystem.h"
#include "AssetRegistry/AssetData.h" #include "AssetRegistry/AssetData.h"
#include "StructUtils/UserDefinedStruct.h"
#include "Engine/UserDefinedEnum.h"
#include "MCPUtils.h" #include "MCPUtils.h"
#include "Engine/Blueprint.h" #include "Engine/Blueprint.h"
#include "Engine/LevelScriptBlueprint.h" #include "Engine/LevelScriptBlueprint.h"
#include "Engine/World.h" #include "Engine/World.h"
#include "MCPAssetFinder.generated.h"
class UMaterial;
class UMaterialInstanceConstant;
class UMaterialFunction;
class UAnimationStateMachineGraph;
class IAssetRegistry;
struct FARFilter; 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> // MCPAssetsBase — non-template base for MCPAssets<T>
// ============================================================ // ============================================================

View File

@@ -98,6 +98,27 @@ public:
static FString UrlDecode(const FString& EncodedString); static FString UrlDecode(const FString& EncodedString);
// ----- Blueprint helpers ----- // ----- 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 UEdGraphNode* FindNodeByGuid(UBlueprint* BP, const FString& GuidString, UEdGraph** OutGraph = nullptr);
static bool SaveBlueprintPackage(UBlueprint* BP); static bool SaveBlueprintPackage(UBlueprint* BP);