Files
integration/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Snapshot.cpp

1349 lines
46 KiB
C++

#include "MCPAssetFinder.h"
#include "BlueprintMCPServer.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "EdGraphSchema_K2.h"
#include "K2Node.h"
#include "K2Node_BreakStruct.h"
#include "K2Node_MakeStruct.h"
#include "K2Node_FunctionEntry.h"
#include "K2Node_EditablePinBase.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonWriter.h"
#include "Serialization/JsonSerializer.h"
#include "Misc/Guid.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "UObject/UObjectIterator.h"
// ============================================================
// Snapshot Helpers
// ============================================================
FString FBlueprintMCPServer::GenerateSnapshotId(const FString& BlueprintName)
{
FString CleanName = BlueprintName;
CleanName.ReplaceInline(TEXT("/"), TEXT("_"));
CleanName.ReplaceInline(TEXT(" "), TEXT("_"));
FString Timestamp = FDateTime::Now().ToString(TEXT("%Y%m%d_%H%M%S"));
return FString::Printf(TEXT("%s_%s_%s"), *CleanName, *Timestamp, *FGuid::NewGuid().ToString().Left(8));
}
FGraphSnapshotData FBlueprintMCPServer::CaptureGraphSnapshot(UEdGraph* Graph)
{
FGraphSnapshotData Data;
if (!Graph) return Data;
// Record all nodes
for (UEdGraphNode* Node : Graph->Nodes)
{
if (!Node) continue;
FNodeRecord Record;
Record.NodeGuid = Node->NodeGuid.ToString();
Record.NodeClass = Node->GetClass()->GetName();
Record.NodeTitle = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
// Check for Break/Make struct type
if (UK2Node_BreakStruct* BreakNode = Cast<UK2Node_BreakStruct>(Node))
{
Record.StructType = BreakNode->StructType ? BreakNode->StructType->GetName() : TEXT("<unknown struct>");
}
else if (UK2Node_MakeStruct* MakeNode = Cast<UK2Node_MakeStruct>(Node))
{
Record.StructType = MakeNode->StructType ? MakeNode->StructType->GetName() : TEXT("<unknown struct>");
}
Data.Nodes.Add(Record);
// Record ALL pin connections (only from output pins to avoid duplicates)
for (UEdGraphPin* Pin : Node->Pins)
{
if (!Pin) continue;
if (Pin->Direction != EGPD_Output) continue;
for (UEdGraphPin* Linked : Pin->LinkedTo)
{
if (!Linked || !Linked->GetOwningNode()) continue;
FPinConnectionRecord ConnRecord;
ConnRecord.SourceNodeGuid = Node->NodeGuid.ToString();
ConnRecord.SourcePinName = Pin->PinName.ToString();
ConnRecord.TargetNodeGuid = Linked->GetOwningNode()->NodeGuid.ToString();
ConnRecord.TargetPinName = Linked->PinName.ToString();
Data.Connections.Add(ConnRecord);
}
}
}
return Data;
}
void FBlueprintMCPServer::PruneOldSnapshots()
{
while (Snapshots.Num() > MaxSnapshots)
{
FString OldestId;
FDateTime OldestTime = FDateTime::MaxValue();
for (const auto& Pair : Snapshots)
{
if (Pair.Value.CreatedAt < OldestTime)
{
OldestTime = Pair.Value.CreatedAt;
OldestId = Pair.Key;
}
}
if (!OldestId.IsEmpty())
{
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Pruning old snapshot '%s'"), *OldestId);
Snapshots.Remove(OldestId);
}
else
{
break;
}
}
}
bool FBlueprintMCPServer::SaveSnapshotToDisk(const FString& SnapshotId, const FGraphSnapshot& Snapshot)
{
FString Dir = FPaths::ProjectSavedDir() / TEXT("BlueprintMCP") / TEXT("Snapshots");
IFileManager::Get().MakeDirectory(*Dir, true);
FString FilePath = Dir / (SnapshotId + TEXT(".json"));
// Serialize to JSON
TSharedRef<FJsonObject> Root = MakeShared<FJsonObject>();
Root->SetStringField(TEXT("snapshotId"), Snapshot.SnapshotId);
Root->SetStringField(TEXT("blueprintName"), Snapshot.BlueprintName);
Root->SetStringField(TEXT("blueprintPath"), Snapshot.BlueprintPath);
Root->SetStringField(TEXT("createdAt"), Snapshot.CreatedAt.ToIso8601());
TSharedRef<FJsonObject> GraphsObj = MakeShared<FJsonObject>();
for (const auto& GraphPair : Snapshot.Graphs)
{
TSharedRef<FJsonObject> GraphObj = MakeShared<FJsonObject>();
// Nodes
TArray<TSharedPtr<FJsonValue>> NodesArr;
for (const FNodeRecord& NodeRec : GraphPair.Value.Nodes)
{
TSharedRef<FJsonObject> NJ = MakeShared<FJsonObject>();
NJ->SetStringField(TEXT("nodeGuid"), NodeRec.NodeGuid);
NJ->SetStringField(TEXT("nodeClass"), NodeRec.NodeClass);
NJ->SetStringField(TEXT("nodeTitle"), NodeRec.NodeTitle);
if (!NodeRec.StructType.IsEmpty())
{
NJ->SetStringField(TEXT("structType"), NodeRec.StructType);
}
NodesArr.Add(MakeShared<FJsonValueObject>(NJ));
}
GraphObj->SetArrayField(TEXT("nodes"), NodesArr);
// Connections
TArray<TSharedPtr<FJsonValue>> ConnsArr;
for (const FPinConnectionRecord& ConnRec : GraphPair.Value.Connections)
{
TSharedRef<FJsonObject> CJ = MakeShared<FJsonObject>();
CJ->SetStringField(TEXT("sourceNodeGuid"), ConnRec.SourceNodeGuid);
CJ->SetStringField(TEXT("sourcePinName"), ConnRec.SourcePinName);
CJ->SetStringField(TEXT("targetNodeGuid"), ConnRec.TargetNodeGuid);
CJ->SetStringField(TEXT("targetPinName"), ConnRec.TargetPinName);
ConnsArr.Add(MakeShared<FJsonValueObject>(CJ));
}
GraphObj->SetArrayField(TEXT("connections"), ConnsArr);
GraphsObj->SetObjectField(GraphPair.Key, GraphObj);
}
Root->SetObjectField(TEXT("graphs"), GraphsObj);
FString JsonString = MCPUtils::JsonToString(Root);
bool bSuccess = FFileHelper::SaveStringToFile(JsonString, *FilePath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM);
if (bSuccess)
{
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Saved snapshot to disk: %s"), *FilePath);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Failed to save snapshot to disk: %s"), *FilePath);
}
return bSuccess;
}
bool FBlueprintMCPServer::LoadSnapshotFromDisk(const FString& SnapshotId, FGraphSnapshot& OutSnapshot)
{
FString Dir = FPaths::ProjectSavedDir() / TEXT("BlueprintMCP") / TEXT("Snapshots");
FString FilePath = Dir / (SnapshotId + TEXT(".json"));
FString JsonString;
if (!FFileHelper::LoadFileToString(JsonString, *FilePath))
{
return false;
}
TSharedPtr<FJsonObject> Root;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonString);
if (!FJsonSerializer::Deserialize(Reader, Root) || !Root.IsValid())
{
return false;
}
OutSnapshot.SnapshotId = Root->GetStringField(TEXT("snapshotId"));
OutSnapshot.BlueprintName = Root->GetStringField(TEXT("blueprintName"));
OutSnapshot.BlueprintPath = Root->GetStringField(TEXT("blueprintPath"));
FDateTime::ParseIso8601(*Root->GetStringField(TEXT("createdAt")), OutSnapshot.CreatedAt);
const TSharedPtr<FJsonObject>* GraphsObjPtr = nullptr;
if (Root->TryGetObjectField(TEXT("graphs"), GraphsObjPtr) && GraphsObjPtr && (*GraphsObjPtr).IsValid())
{
for (const auto& GraphPair : (*GraphsObjPtr)->Values)
{
FGraphSnapshotData GraphData;
const TSharedPtr<FJsonObject>& GraphObj = GraphPair.Value->AsObject();
if (!GraphObj.IsValid()) continue;
// Nodes
const TArray<TSharedPtr<FJsonValue>>* NodesArrPtr = nullptr;
if (GraphObj->TryGetArrayField(TEXT("nodes"), NodesArrPtr))
{
for (const TSharedPtr<FJsonValue>& NodeVal : *NodesArrPtr)
{
const TSharedPtr<FJsonObject>& NJ = NodeVal->AsObject();
if (!NJ.IsValid()) continue;
FNodeRecord NodeRec;
NodeRec.NodeGuid = NJ->GetStringField(TEXT("nodeGuid"));
NodeRec.NodeClass = NJ->GetStringField(TEXT("nodeClass"));
NodeRec.NodeTitle = NJ->GetStringField(TEXT("nodeTitle"));
NJ->TryGetStringField(TEXT("structType"), NodeRec.StructType);
GraphData.Nodes.Add(NodeRec);
}
}
// Connections
const TArray<TSharedPtr<FJsonValue>>* ConnsArrPtr = nullptr;
if (GraphObj->TryGetArrayField(TEXT("connections"), ConnsArrPtr))
{
for (const TSharedPtr<FJsonValue>& ConnVal : *ConnsArrPtr)
{
const TSharedPtr<FJsonObject>& CJ = ConnVal->AsObject();
if (!CJ.IsValid()) continue;
FPinConnectionRecord ConnRec;
ConnRec.SourceNodeGuid = CJ->GetStringField(TEXT("sourceNodeGuid"));
ConnRec.SourcePinName = CJ->GetStringField(TEXT("sourcePinName"));
ConnRec.TargetNodeGuid = CJ->GetStringField(TEXT("targetNodeGuid"));
ConnRec.TargetPinName = CJ->GetStringField(TEXT("targetPinName"));
GraphData.Connections.Add(ConnRec);
}
}
OutSnapshot.Graphs.Add(GraphPair.Key, GraphData);
}
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Loaded snapshot from disk: %s"), *FilePath);
return true;
}
// ============================================================
// HandleSnapshotGraph
// ============================================================
void FBlueprintMCPServer::HandleSnapshotGraph(const FJsonObject* Json, FJsonObject* Result)
{
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
if (BlueprintName.IsEmpty())
{
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint"));
}
FString GraphFilter;
Json->TryGetStringField(TEXT("graph"), GraphFilter);
// Load Blueprint
FString LoadError;
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintByName(BlueprintName, LoadError);
if (!BP)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating snapshot for blueprint '%s'"), *BlueprintName);
// Build the snapshot
FGraphSnapshot Snapshot;
Snapshot.SnapshotId = GenerateSnapshotId(BlueprintName);
Snapshot.BlueprintName = BP->GetName();
Snapshot.BlueprintPath = BP->GetPathName();
Snapshot.CreatedAt = FDateTime::Now();
// Gather all graphs (UbergraphPages + FunctionGraphs)
TArray<UEdGraph*> GraphsToCapture;
for (UEdGraph* Graph : BP->UbergraphPages)
{
if (!Graph) continue;
if (!GraphFilter.IsEmpty() && Graph->GetName() != GraphFilter) continue;
GraphsToCapture.Add(Graph);
}
for (UEdGraph* Graph : BP->FunctionGraphs)
{
if (!Graph) continue;
if (!GraphFilter.IsEmpty() && Graph->GetName() != GraphFilter) continue;
GraphsToCapture.Add(Graph);
}
if (GraphsToCapture.Num() == 0 && !GraphFilter.IsEmpty())
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found in blueprint '%s'"), *GraphFilter, *BlueprintName));
}
int32 TotalConnections = 0;
TArray<TSharedPtr<FJsonValue>> GraphSummaries;
for (UEdGraph* Graph : GraphsToCapture)
{
FGraphSnapshotData GraphData = CaptureGraphSnapshot(Graph);
TSharedRef<FJsonObject> Summary = MakeShared<FJsonObject>();
Summary->SetStringField(TEXT("name"), Graph->GetName());
Summary->SetNumberField(TEXT("nodeCount"), GraphData.Nodes.Num());
Summary->SetNumberField(TEXT("connectionCount"), GraphData.Connections.Num());
GraphSummaries.Add(MakeShared<FJsonValueObject>(Summary));
TotalConnections += GraphData.Connections.Num();
Snapshot.Graphs.Add(Graph->GetName(), MoveTemp(GraphData));
}
// Store in memory
Snapshots.Add(Snapshot.SnapshotId, Snapshot);
PruneOldSnapshots();
// Save to disk
SaveSnapshotToDisk(Snapshot.SnapshotId, Snapshot);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Snapshot '%s' created with %d graphs, %d total connections"),
*Snapshot.SnapshotId, GraphsToCapture.Num(), TotalConnections);
// Build response
Result->SetStringField(TEXT("status"), TEXT("ok"));
Result->SetStringField(TEXT("snapshotId"), Snapshot.SnapshotId);
Result->SetStringField(TEXT("blueprint"), BP->GetName());
Result->SetArrayField(TEXT("graphs"), GraphSummaries);
Result->SetNumberField(TEXT("totalConnections"), TotalConnections);
}
// ============================================================
// HandleDiffGraph
// ============================================================
void FBlueprintMCPServer::HandleDiffGraph(const FJsonObject* Json, FJsonObject* Result)
{
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
FString SnapshotId = Json->GetStringField(TEXT("snapshotId"));
if (BlueprintName.IsEmpty() || SnapshotId.IsEmpty())
{
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, snapshotId"));
}
FString GraphFilter;
Json->TryGetStringField(TEXT("graph"), GraphFilter);
// Load snapshot from memory or disk
FGraphSnapshot* SnapshotPtr = Snapshots.Find(SnapshotId);
FGraphSnapshot LoadedSnapshot;
if (!SnapshotPtr)
{
if (!LoadSnapshotFromDisk(SnapshotId, LoadedSnapshot))
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Snapshot '%s' not found in memory or on disk"), *SnapshotId));
}
SnapshotPtr = &LoadedSnapshot;
}
// Load the current blueprint
FString LoadError;
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintByName(BlueprintName, LoadError);
if (!BP)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Diffing blueprint '%s' against snapshot '%s'"), *BlueprintName, *SnapshotId);
// Build current state for comparison
TMap<FString, FGraphSnapshotData> CurrentGraphs;
TArray<UEdGraph*> AllGraphs;
for (UEdGraph* Graph : BP->UbergraphPages)
{
if (Graph) AllGraphs.Add(Graph);
}
for (UEdGraph* Graph : BP->FunctionGraphs)
{
if (Graph) AllGraphs.Add(Graph);
}
for (UEdGraph* Graph : AllGraphs)
{
if (!GraphFilter.IsEmpty() && Graph->GetName() != GraphFilter) continue;
CurrentGraphs.Add(Graph->GetName(), CaptureGraphSnapshot(Graph));
}
// Build lookup maps for current state
// Key: "GraphName|SourceGuid|SourcePin|TargetGuid|TargetPin"
auto MakeConnKey = [](const FString& SrcGuid, const FString& SrcPin, const FString& TgtGuid, const FString& TgtPin) -> FString
{
return FString::Printf(TEXT("%s|%s|%s|%s"), *SrcGuid, *SrcPin, *TgtGuid, *TgtPin);
};
// Build node lookup maps: GUID -> NodeRecord
TMap<FString, const FNodeRecord*> SnapshotNodeMap;
TMap<FString, const FNodeRecord*> CurrentNodeMap;
TArray<TSharedPtr<FJsonValue>> SeveredArr;
TArray<TSharedPtr<FJsonValue>> NewConnsArr;
TArray<TSharedPtr<FJsonValue>> TypeChangesArr;
TArray<TSharedPtr<FJsonValue>> MissingNodesArr;
// Process each graph in the snapshot
for (const auto& SnapGraphPair : SnapshotPtr->Graphs)
{
const FString& GraphName = SnapGraphPair.Key;
if (!GraphFilter.IsEmpty() && GraphName != GraphFilter) continue;
const FGraphSnapshotData& SnapData = SnapGraphPair.Value;
const FGraphSnapshotData* CurDataPtr = CurrentGraphs.Find(GraphName);
// Build snapshot node map for this graph
TMap<FString, const FNodeRecord*> SnapNodeLookup;
for (const FNodeRecord& NR : SnapData.Nodes)
{
SnapNodeLookup.Add(NR.NodeGuid, &NR);
}
// Build current connection set and node map for this graph
TSet<FString> CurrentConnSet;
TMap<FString, const FNodeRecord*> CurNodeLookup;
if (CurDataPtr)
{
for (const FNodeRecord& NR : CurDataPtr->Nodes)
{
CurNodeLookup.Add(NR.NodeGuid, &NR);
}
for (const FPinConnectionRecord& Conn : CurDataPtr->Connections)
{
CurrentConnSet.Add(MakeConnKey(Conn.SourceNodeGuid, Conn.SourcePinName, Conn.TargetNodeGuid, Conn.TargetPinName));
}
}
// Build snapshot connection set
TSet<FString> SnapConnSet;
for (const FPinConnectionRecord& Conn : SnapData.Connections)
{
SnapConnSet.Add(MakeConnKey(Conn.SourceNodeGuid, Conn.SourcePinName, Conn.TargetNodeGuid, Conn.TargetPinName));
}
// Find severed connections: in snapshot but not in current
for (const FPinConnectionRecord& Conn : SnapData.Connections)
{
FString Key = MakeConnKey(Conn.SourceNodeGuid, Conn.SourcePinName, Conn.TargetNodeGuid, Conn.TargetPinName);
if (!CurrentConnSet.Contains(Key))
{
TSharedRef<FJsonObject> SJ = MakeShared<FJsonObject>();
SJ->SetStringField(TEXT("graph"), GraphName);
SJ->SetStringField(TEXT("sourceNodeGuid"), Conn.SourceNodeGuid);
SJ->SetStringField(TEXT("sourcePinName"), Conn.SourcePinName);
SJ->SetStringField(TEXT("targetNodeGuid"), Conn.TargetNodeGuid);
SJ->SetStringField(TEXT("targetPinName"), Conn.TargetPinName);
// Add node names for readability
const FNodeRecord** SrcRec = SnapNodeLookup.Find(Conn.SourceNodeGuid);
if (SrcRec) SJ->SetStringField(TEXT("sourceNodeName"), (*SrcRec)->NodeTitle);
const FNodeRecord** TgtRec = SnapNodeLookup.Find(Conn.TargetNodeGuid);
if (TgtRec) SJ->SetStringField(TEXT("targetNodeName"), (*TgtRec)->NodeTitle);
SeveredArr.Add(MakeShared<FJsonValueObject>(SJ));
}
}
// Find new connections: in current but not in snapshot
if (CurDataPtr)
{
for (const FPinConnectionRecord& Conn : CurDataPtr->Connections)
{
FString Key = MakeConnKey(Conn.SourceNodeGuid, Conn.SourcePinName, Conn.TargetNodeGuid, Conn.TargetPinName);
if (!SnapConnSet.Contains(Key))
{
TSharedRef<FJsonObject> NJ = MakeShared<FJsonObject>();
NJ->SetStringField(TEXT("graph"), GraphName);
NJ->SetStringField(TEXT("sourceNodeGuid"), Conn.SourceNodeGuid);
NJ->SetStringField(TEXT("sourcePinName"), Conn.SourcePinName);
NJ->SetStringField(TEXT("targetNodeGuid"), Conn.TargetNodeGuid);
NJ->SetStringField(TEXT("targetPinName"), Conn.TargetPinName);
const FNodeRecord** SrcRec = CurNodeLookup.Find(Conn.SourceNodeGuid);
if (SrcRec) NJ->SetStringField(TEXT("sourceNodeName"), (*SrcRec)->NodeTitle);
const FNodeRecord** TgtRec = CurNodeLookup.Find(Conn.TargetNodeGuid);
if (TgtRec) NJ->SetStringField(TEXT("targetNodeName"), (*TgtRec)->NodeTitle);
NewConnsArr.Add(MakeShared<FJsonValueObject>(NJ));
}
}
}
// Find type changes and missing nodes
for (const FNodeRecord& SnapNode : SnapData.Nodes)
{
const FNodeRecord** CurNodePtr = CurNodeLookup.Find(SnapNode.NodeGuid);
if (!CurNodePtr)
{
// Missing node
TSharedRef<FJsonObject> MJ = MakeShared<FJsonObject>();
MJ->SetStringField(TEXT("graph"), GraphName);
MJ->SetStringField(TEXT("nodeGuid"), SnapNode.NodeGuid);
MJ->SetStringField(TEXT("nodeClass"), SnapNode.NodeClass);
MJ->SetStringField(TEXT("nodeTitle"), SnapNode.NodeTitle);
if (!SnapNode.StructType.IsEmpty())
{
MJ->SetStringField(TEXT("structType"), SnapNode.StructType);
}
MissingNodesArr.Add(MakeShared<FJsonValueObject>(MJ));
}
else if (!SnapNode.StructType.IsEmpty())
{
// Check for type change on Break/Make nodes
const FNodeRecord* CurNode = *CurNodePtr;
if (CurNode->StructType != SnapNode.StructType)
{
TSharedRef<FJsonObject> TJ = MakeShared<FJsonObject>();
TJ->SetStringField(TEXT("graph"), GraphName);
TJ->SetStringField(TEXT("nodeGuid"), SnapNode.NodeGuid);
TJ->SetStringField(TEXT("nodeTitle"), SnapNode.NodeTitle);
TJ->SetStringField(TEXT("oldStructType"), SnapNode.StructType);
TJ->SetStringField(TEXT("newStructType"), CurNode->StructType);
TypeChangesArr.Add(MakeShared<FJsonValueObject>(TJ));
}
}
}
}
// Build result
Result->SetStringField(TEXT("status"), TEXT("ok"));
Result->SetStringField(TEXT("blueprint"), BP->GetName());
Result->SetStringField(TEXT("snapshotId"), SnapshotId);
Result->SetArrayField(TEXT("severedConnections"), SeveredArr);
Result->SetArrayField(TEXT("newConnections"), NewConnsArr);
Result->SetArrayField(TEXT("typeChanges"), TypeChangesArr);
Result->SetArrayField(TEXT("missingNodes"), MissingNodesArr);
TSharedRef<FJsonObject> Summary = MakeShared<FJsonObject>();
Summary->SetNumberField(TEXT("severedConnections"), SeveredArr.Num());
Summary->SetNumberField(TEXT("newConnections"), NewConnsArr.Num());
Summary->SetNumberField(TEXT("typeChanges"), TypeChangesArr.Num());
Summary->SetNumberField(TEXT("missingNodes"), MissingNodesArr.Num());
Result->SetObjectField(TEXT("summary"), Summary);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Diff complete — %d severed, %d new, %d type changes, %d missing nodes"),
SeveredArr.Num(), NewConnsArr.Num(), TypeChangesArr.Num(), MissingNodesArr.Num());
}
// ============================================================
// HandleRestoreGraph
// ============================================================
void FBlueprintMCPServer::HandleRestoreGraph(const FJsonObject* Json, FJsonObject* Result)
{
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
FString SnapshotId = Json->GetStringField(TEXT("snapshotId"));
if (BlueprintName.IsEmpty() || SnapshotId.IsEmpty())
{
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, snapshotId"));
}
FString GraphFilter;
Json->TryGetStringField(TEXT("graph"), GraphFilter);
FString NodeIdFilter;
Json->TryGetStringField(TEXT("nodeId"), NodeIdFilter);
bool bDryRun = false;
Json->TryGetBoolField(TEXT("dryRun"), bDryRun);
// Load snapshot from memory or disk
FGraphSnapshot* SnapshotPtr = Snapshots.Find(SnapshotId);
FGraphSnapshot LoadedSnapshot;
if (!SnapshotPtr)
{
if (!LoadSnapshotFromDisk(SnapshotId, LoadedSnapshot))
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Snapshot '%s' not found in memory or on disk"), *SnapshotId));
}
SnapshotPtr = &LoadedSnapshot;
}
// Load the current blueprint
FString LoadError;
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintByName(BlueprintName, LoadError);
if (!BP)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Restoring connections from snapshot '%s' for blueprint '%s' (dryRun=%s)"),
*SnapshotId, *BlueprintName, bDryRun ? TEXT("true") : TEXT("false"));
// Build current connection set for comparison
TMap<FString, FGraphSnapshotData> CurrentGraphs;
TArray<UEdGraph*> AllGraphs;
for (UEdGraph* Graph : BP->UbergraphPages)
{
if (Graph) AllGraphs.Add(Graph);
}
for (UEdGraph* Graph : BP->FunctionGraphs)
{
if (Graph) AllGraphs.Add(Graph);
}
for (UEdGraph* Graph : AllGraphs)
{
CurrentGraphs.Add(Graph->GetName(), CaptureGraphSnapshot(Graph));
}
auto MakeConnKey = [](const FString& SrcGuid, const FString& SrcPin, const FString& TgtGuid, const FString& TgtPin) -> FString
{
return FString::Printf(TEXT("%s|%s|%s|%s"), *SrcGuid, *SrcPin, *TgtGuid, *TgtPin);
};
int32 Reconnected = 0;
int32 Failed = 0;
TArray<TSharedPtr<FJsonValue>> DetailsArr;
for (const auto& SnapGraphPair : SnapshotPtr->Graphs)
{
const FString& GraphName = SnapGraphPair.Key;
if (!GraphFilter.IsEmpty() && GraphName != GraphFilter) continue;
const FGraphSnapshotData& SnapData = SnapGraphPair.Value;
const FGraphSnapshotData* CurDataPtr = CurrentGraphs.Find(GraphName);
// Build current connection set
TSet<FString> CurrentConnSet;
if (CurDataPtr)
{
for (const FPinConnectionRecord& Conn : CurDataPtr->Connections)
{
CurrentConnSet.Add(MakeConnKey(Conn.SourceNodeGuid, Conn.SourcePinName, Conn.TargetNodeGuid, Conn.TargetPinName));
}
}
// Find severed connections and try to restore them
for (const FPinConnectionRecord& Conn : SnapData.Connections)
{
FString Key = MakeConnKey(Conn.SourceNodeGuid, Conn.SourcePinName, Conn.TargetNodeGuid, Conn.TargetPinName);
if (CurrentConnSet.Contains(Key)) continue; // Still connected, skip
// Apply nodeId filter if specified
if (!NodeIdFilter.IsEmpty() && Conn.SourceNodeGuid != NodeIdFilter && Conn.TargetNodeGuid != NodeIdFilter)
{
continue;
}
TSharedRef<FJsonObject> Detail = MakeShared<FJsonObject>();
Detail->SetStringField(TEXT("graph"), GraphName);
Detail->SetStringField(TEXT("sourcePinName"), Conn.SourcePinName);
Detail->SetStringField(TEXT("targetPinName"), Conn.TargetPinName);
Detail->SetStringField(TEXT("sourceNodeGuid"), Conn.SourceNodeGuid);
Detail->SetStringField(TEXT("targetNodeGuid"), Conn.TargetNodeGuid);
// Find source and target nodes
UEdGraph* SourceGraph = nullptr;
UEdGraphNode* SourceNode = MCPUtils::FindNodeByGuid(BP, Conn.SourceNodeGuid, &SourceGraph);
UEdGraphNode* TargetNode = MCPUtils::FindNodeByGuid(BP, Conn.TargetNodeGuid);
if (!SourceNode)
{
Detail->SetStringField(TEXT("result"), TEXT("failed"));
Detail->SetStringField(TEXT("reason"), FString::Printf(TEXT("Source node '%s' no longer exists"), *Conn.SourceNodeGuid));
Failed++;
DetailsArr.Add(MakeShared<FJsonValueObject>(Detail));
continue;
}
if (!TargetNode)
{
Detail->SetStringField(TEXT("result"), TEXT("failed"));
Detail->SetStringField(TEXT("reason"), FString::Printf(TEXT("Target node '%s' no longer exists"), *Conn.TargetNodeGuid));
Failed++;
DetailsArr.Add(MakeShared<FJsonValueObject>(Detail));
continue;
}
Detail->SetStringField(TEXT("sourceNodeName"), SourceNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString());
Detail->SetStringField(TEXT("targetNodeName"), TargetNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString());
// Find pins
UEdGraphPin* SourcePin = SourceNode->FindPin(FName(*Conn.SourcePinName));
UEdGraphPin* TargetPin = TargetNode->FindPin(FName(*Conn.TargetPinName));
if (!SourcePin)
{
Detail->SetStringField(TEXT("result"), TEXT("failed"));
Detail->SetStringField(TEXT("reason"), FString::Printf(TEXT("Source pin '%s' not found on node"), *Conn.SourcePinName));
Failed++;
DetailsArr.Add(MakeShared<FJsonValueObject>(Detail));
continue;
}
if (!TargetPin)
{
Detail->SetStringField(TEXT("result"), TEXT("failed"));
Detail->SetStringField(TEXT("reason"), FString::Printf(TEXT("Target pin '%s' not found on node"), *Conn.TargetPinName));
Failed++;
DetailsArr.Add(MakeShared<FJsonValueObject>(Detail));
continue;
}
if (bDryRun)
{
// In dry run, just report what would happen
Detail->SetStringField(TEXT("result"), TEXT("would_reconnect"));
Reconnected++;
DetailsArr.Add(MakeShared<FJsonValueObject>(Detail));
continue;
}
// Try type-validated connection via the schema
const UEdGraphSchema* Schema = SourceGraph ? SourceGraph->GetSchema() : nullptr;
if (!Schema)
{
Detail->SetStringField(TEXT("result"), TEXT("failed"));
Detail->SetStringField(TEXT("reason"), TEXT("Graph schema not found"));
Failed++;
DetailsArr.Add(MakeShared<FJsonValueObject>(Detail));
continue;
}
bool bConnected = Schema->TryCreateConnection(SourcePin, TargetPin);
if (bConnected)
{
Detail->SetStringField(TEXT("result"), TEXT("reconnected"));
Reconnected++;
}
else
{
Detail->SetStringField(TEXT("result"), TEXT("failed"));
Detail->SetStringField(TEXT("reason"), FString::Printf(
TEXT("TryCreateConnection failed — types may be incompatible (%s -> %s)"),
*SourcePin->PinType.PinCategory.ToString(),
*TargetPin->PinType.PinCategory.ToString()));
Failed++;
}
DetailsArr.Add(MakeShared<FJsonValueObject>(Detail));
}
}
// Save if not dry run and we reconnected something
bool bSaved = false;
if (!bDryRun && Reconnected > 0)
{
bSaved = MCPUtils::SaveBlueprintPackage(BP);
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Restore complete — %d reconnected, %d failed, saved=%s"),
Reconnected, Failed, bSaved ? TEXT("true") : TEXT("false"));
Result->SetStringField(TEXT("status"), TEXT("ok"));
Result->SetNumberField(TEXT("reconnected"), Reconnected);
Result->SetNumberField(TEXT("failed"), Failed);
Result->SetArrayField(TEXT("details"), DetailsArr);
Result->SetBoolField(TEXT("saved"), bSaved);
Result->SetBoolField(TEXT("dryRun"), bDryRun);
}
// ============================================================
// HandleFindDisconnectedPins
// ============================================================
void FBlueprintMCPServer::HandleFindDisconnectedPins(const FJsonObject* Json, FJsonObject* Result)
{
FString BlueprintName;
Json->TryGetStringField(TEXT("blueprint"), BlueprintName);
FString PathFilter;
Json->TryGetStringField(TEXT("filter"), PathFilter);
FString SnapshotId;
Json->TryGetStringField(TEXT("snapshotId"), SnapshotId);
if (BlueprintName.IsEmpty() && PathFilter.IsEmpty() && SnapshotId.IsEmpty())
{
return MCPUtils::MakeErrorJson(Result, TEXT("Provide at least one of: blueprint, filter, or snapshotId"));
}
// Optionally load snapshot for definite-break detection
FGraphSnapshot* SnapshotPtr = nullptr;
FGraphSnapshot LoadedSnapshot;
if (!SnapshotId.IsEmpty())
{
SnapshotPtr = Snapshots.Find(SnapshotId);
if (!SnapshotPtr)
{
if (LoadSnapshotFromDisk(SnapshotId, LoadedSnapshot))
{
SnapshotPtr = &LoadedSnapshot;
}
}
}
// Build snapshot connection lookup: "nodeGuid|pinName" -> array of connected targets
TMap<FString, TArray<FPinConnectionRecord>> SnapConnByNode;
if (SnapshotPtr)
{
for (const auto& GraphPair : SnapshotPtr->Graphs)
{
for (const FPinConnectionRecord& Conn : GraphPair.Value.Connections)
{
FString SrcKey = FString::Printf(TEXT("%s|%s"), *Conn.SourceNodeGuid, *Conn.SourcePinName);
SnapConnByNode.FindOrAdd(SrcKey).Add(Conn);
FString TgtKey = FString::Printf(TEXT("%s|%s"), *Conn.TargetNodeGuid, *Conn.TargetPinName);
SnapConnByNode.FindOrAdd(TgtKey).Add(Conn);
}
}
}
// Collect blueprints to scan
TArray<FString> BlueprintsToScan;
if (!BlueprintName.IsEmpty())
{
BlueprintsToScan.Add(BlueprintName);
}
else if (!PathFilter.IsEmpty())
{
for (const FAssetData& Asset : UMCPAssetFinder::GetBlueprintAssets())
{
if (Asset.PackageName.ToString().Contains(PathFilter) || Asset.AssetName.ToString().Contains(PathFilter))
{
BlueprintsToScan.Add(Asset.AssetName.ToString());
}
}
}
else if (SnapshotPtr)
{
// Use the snapshot's blueprint
BlueprintsToScan.Add(SnapshotPtr->BlueprintName);
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Finding disconnected pins across %d blueprint(s)"), BlueprintsToScan.Num());
TArray<TSharedPtr<FJsonValue>> ResultsArr;
int32 HighCount = 0;
int32 MediumCount = 0;
int32 BlueprintsScanned = 0;
for (const FString& BPName : BlueprintsToScan)
{
FString LoadError;
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintByName(BPName, LoadError);
if (!BP) continue;
BlueprintsScanned++;
TArray<UEdGraph*> AllGraphs;
for (UEdGraph* Graph : BP->UbergraphPages)
{
if (Graph) AllGraphs.Add(Graph);
}
for (UEdGraph* Graph : BP->FunctionGraphs)
{
if (Graph) AllGraphs.Add(Graph);
}
for (UEdGraph* Graph : AllGraphs)
{
for (UEdGraphNode* Node : Graph->Nodes)
{
if (!Node) continue;
UK2Node_BreakStruct* BreakNode = Cast<UK2Node_BreakStruct>(Node);
UK2Node_MakeStruct* MakeNode = Cast<UK2Node_MakeStruct>(Node);
// Only inspect Break/Make struct nodes for heuristic detection
bool bIsBreakMakeNode = (BreakNode || MakeNode);
FString StructTypeName;
if (BreakNode)
{
StructTypeName = BreakNode->StructType ? BreakNode->StructType->GetName() : TEXT("<unknown struct>");
}
else if (MakeNode)
{
StructTypeName = MakeNode->StructType ? MakeNode->StructType->GetName() : TEXT("<unknown struct>");
}
// Heuristic detection for Break/Make nodes
if (bIsBreakMakeNode)
{
bool bUnresolvedStruct = StructTypeName.Contains(TEXT("unknown")) ||
StructTypeName.Equals(TEXT("None")) || StructTypeName.IsEmpty();
if (bUnresolvedStruct)
{
// HIGH confidence: unresolved struct
TSharedRef<FJsonObject> Item = MakeShared<FJsonObject>();
Item->SetStringField(TEXT("blueprint"), BP->GetName());
Item->SetStringField(TEXT("graph"), Graph->GetName());
Item->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
Item->SetStringField(TEXT("nodeTitle"), Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString());
Item->SetStringField(TEXT("structType"), StructTypeName);
Item->SetStringField(TEXT("confidence"), TEXT("HIGH"));
Item->SetStringField(TEXT("reason"), TEXT("Unresolved or unknown struct type"));
// List pins
TArray<TSharedPtr<FJsonValue>> PinsArr;
for (UEdGraphPin* Pin : Node->Pins)
{
if (!Pin || Pin->bHidden) continue;
if (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec) continue;
TSharedRef<FJsonObject> PinJ = MakeShared<FJsonObject>();
PinJ->SetStringField(TEXT("name"), Pin->PinName.ToString());
PinJ->SetStringField(TEXT("type"), Pin->PinType.PinCategory.ToString());
// Check snapshot for what it was connected to
FString PinKey = FString::Printf(TEXT("%s|%s"), *Node->NodeGuid.ToString(), *Pin->PinName.ToString());
if (SnapConnByNode.Contains(PinKey))
{
TArray<TSharedPtr<FJsonValue>> WasConnArr;
for (const FPinConnectionRecord& CR : SnapConnByNode[PinKey])
{
FString OtherGuid = (CR.SourceNodeGuid == Node->NodeGuid.ToString()) ? CR.TargetNodeGuid : CR.SourceNodeGuid;
FString OtherPin = (CR.SourceNodeGuid == Node->NodeGuid.ToString()) ? CR.TargetPinName : CR.SourcePinName;
WasConnArr.Add(MakeShared<FJsonValueString>(FString::Printf(TEXT("%s.%s"), *OtherGuid, *OtherPin)));
}
PinJ->SetArrayField(TEXT("wasConnectedTo"), WasConnArr);
}
PinsArr.Add(MakeShared<FJsonValueObject>(PinJ));
}
Item->SetArrayField(TEXT("pins"), PinsArr);
ResultsArr.Add(MakeShared<FJsonValueObject>(Item));
HighCount++;
}
else
{
// Check for MEDIUM: valid struct but zero data pin connections
bool bHasDataConnection = false;
for (UEdGraphPin* Pin : Node->Pins)
{
if (!Pin || Pin->bHidden) continue;
if (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec) continue;
if (Pin->LinkedTo.Num() > 0)
{
bHasDataConnection = true;
break;
}
}
if (!bHasDataConnection)
{
TSharedRef<FJsonObject> Item = MakeShared<FJsonObject>();
Item->SetStringField(TEXT("blueprint"), BP->GetName());
Item->SetStringField(TEXT("graph"), Graph->GetName());
Item->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
Item->SetStringField(TEXT("nodeTitle"), Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString());
Item->SetStringField(TEXT("structType"), StructTypeName);
Item->SetStringField(TEXT("confidence"), TEXT("MEDIUM"));
Item->SetStringField(TEXT("reason"), TEXT("Break/Make node with valid struct but zero data pin connections"));
TArray<TSharedPtr<FJsonValue>> PinsArr;
for (UEdGraphPin* Pin : Node->Pins)
{
if (!Pin || Pin->bHidden) continue;
if (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec) continue;
TSharedRef<FJsonObject> PinJ = MakeShared<FJsonObject>();
PinJ->SetStringField(TEXT("name"), Pin->PinName.ToString());
PinJ->SetStringField(TEXT("type"), Pin->PinType.PinCategory.ToString());
FString PinKey = FString::Printf(TEXT("%s|%s"), *Node->NodeGuid.ToString(), *Pin->PinName.ToString());
if (SnapConnByNode.Contains(PinKey))
{
TArray<TSharedPtr<FJsonValue>> WasConnArr;
for (const FPinConnectionRecord& CR : SnapConnByNode[PinKey])
{
FString OtherGuid = (CR.SourceNodeGuid == Node->NodeGuid.ToString()) ? CR.TargetNodeGuid : CR.SourceNodeGuid;
FString OtherPin = (CR.SourceNodeGuid == Node->NodeGuid.ToString()) ? CR.TargetPinName : CR.SourcePinName;
WasConnArr.Add(MakeShared<FJsonValueString>(FString::Printf(TEXT("%s.%s"), *OtherGuid, *OtherPin)));
}
PinJ->SetArrayField(TEXT("wasConnectedTo"), WasConnArr);
}
PinsArr.Add(MakeShared<FJsonValueObject>(PinJ));
}
Item->SetArrayField(TEXT("pins"), PinsArr);
ResultsArr.Add(MakeShared<FJsonValueObject>(Item));
MediumCount++;
}
}
}
// Snapshot-based definite-break detection (applies to ALL node types)
if (SnapshotPtr)
{
for (UEdGraphPin* Pin : Node->Pins)
{
if (!Pin || Pin->bHidden) continue;
FString PinKey = FString::Printf(TEXT("%s|%s"), *Node->NodeGuid.ToString(), *Pin->PinName.ToString());
if (!SnapConnByNode.Contains(PinKey)) continue;
// This pin had connections in the snapshot — check if any are now missing
for (const FPinConnectionRecord& CR : SnapConnByNode[PinKey])
{
// Determine which side is "other"
bool bWeAreSource = (CR.SourceNodeGuid == Node->NodeGuid.ToString() && CR.SourcePinName == Pin->PinName.ToString());
FString OtherGuid = bWeAreSource ? CR.TargetNodeGuid : CR.SourceNodeGuid;
FString OtherPinName = bWeAreSource ? CR.TargetPinName : CR.SourcePinName;
// Check if this connection still exists
bool bStillConnected = false;
for (UEdGraphPin* Linked : Pin->LinkedTo)
{
if (Linked && Linked->GetOwningNode() &&
Linked->GetOwningNode()->NodeGuid.ToString() == OtherGuid &&
Linked->PinName.ToString() == OtherPinName)
{
bStillConnected = true;
break;
}
}
if (!bStillConnected)
{
// Skip if we already reported this node above via heuristic
// Only add for non-Break/Make nodes, or if the Break/Make node wasn't caught by heuristics
if (!bIsBreakMakeNode)
{
TSharedRef<FJsonObject> Item = MakeShared<FJsonObject>();
Item->SetStringField(TEXT("blueprint"), BP->GetName());
Item->SetStringField(TEXT("graph"), Graph->GetName());
Item->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
Item->SetStringField(TEXT("nodeTitle"), Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString());
if (!StructTypeName.IsEmpty())
{
Item->SetStringField(TEXT("structType"), StructTypeName);
}
Item->SetStringField(TEXT("confidence"), TEXT("HIGH"));
Item->SetStringField(TEXT("reason"), TEXT("Connection existed in snapshot but is now missing"));
TArray<TSharedPtr<FJsonValue>> PinsArr;
TSharedRef<FJsonObject> PinJ = MakeShared<FJsonObject>();
PinJ->SetStringField(TEXT("name"), Pin->PinName.ToString());
PinJ->SetStringField(TEXT("type"), Pin->PinType.PinCategory.ToString());
TArray<TSharedPtr<FJsonValue>> WasConnArr;
WasConnArr.Add(MakeShared<FJsonValueString>(FString::Printf(TEXT("%s.%s"), *OtherGuid, *OtherPinName)));
PinJ->SetArrayField(TEXT("wasConnectedTo"), WasConnArr);
PinsArr.Add(MakeShared<FJsonValueObject>(PinJ));
Item->SetArrayField(TEXT("pins"), PinsArr);
ResultsArr.Add(MakeShared<FJsonValueObject>(Item));
HighCount++;
}
break; // Only report once per node per snapshot-check pass
}
}
}
}
}
}
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: FindDisconnectedPins complete — %d HIGH, %d MEDIUM, %d total across %d blueprints"),
HighCount, MediumCount, ResultsArr.Num(), BlueprintsScanned);
Result->SetArrayField(TEXT("results"), ResultsArr);
TSharedRef<FJsonObject> Summary = MakeShared<FJsonObject>();
Summary->SetNumberField(TEXT("high"), HighCount);
Summary->SetNumberField(TEXT("medium"), MediumCount);
Summary->SetNumberField(TEXT("total"), ResultsArr.Num());
Summary->SetNumberField(TEXT("blueprintsScanned"), BlueprintsScanned);
Result->SetObjectField(TEXT("summary"), Summary);
}
// ============================================================
// HandleAnalyzeRebuildImpact
// ============================================================
void FBlueprintMCPServer::HandleAnalyzeRebuildImpact(const FJsonObject* Json, FJsonObject* Result)
{
FString ModuleName = Json->GetStringField(TEXT("moduleName"));
if (ModuleName.IsEmpty())
{
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: moduleName"));
}
// Optional struct name filter
TArray<FString> StructNameFilter;
const TArray<TSharedPtr<FJsonValue>>* StructNamesArr = nullptr;
if (Json->TryGetArrayField(TEXT("structNames"), StructNamesArr))
{
for (const TSharedPtr<FJsonValue>& Val : *StructNamesArr)
{
FString Name = Val->AsString();
if (!Name.IsEmpty())
{
StructNameFilter.Add(Name);
}
}
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Analyzing rebuild impact for module '%s'"), *ModuleName);
// Enumerate all UScriptStruct and UEnum objects belonging to this module
TArray<UScriptStruct*> FoundStructs;
TArray<UEnum*> FoundEnums;
for (TObjectIterator<UScriptStruct> It; It; ++It)
{
UScriptStruct* Struct = *It;
if (!Struct) continue;
FString PackageName = Struct->GetOutermost()->GetName();
if (!PackageName.Contains(ModuleName)) continue;
// Apply name filter if provided
if (StructNameFilter.Num() > 0)
{
bool bMatch = false;
for (const FString& FilterName : StructNameFilter)
{
// Match with or without F prefix
FString CleanFilter = FilterName;
if (CleanFilter.StartsWith(TEXT("F")))
{
CleanFilter = CleanFilter.Mid(1);
}
if (Struct->GetName() == FilterName || Struct->GetName() == CleanFilter)
{
bMatch = true;
break;
}
}
if (!bMatch) continue;
}
FoundStructs.Add(Struct);
}
for (TObjectIterator<UEnum> It; It; ++It)
{
UEnum* Enum = *It;
if (!Enum) continue;
FString PackageName = Enum->GetOutermost()->GetName();
if (!PackageName.Contains(ModuleName)) continue;
if (StructNameFilter.Num() > 0)
{
bool bMatch = false;
for (const FString& FilterName : StructNameFilter)
{
FString CleanFilter = FilterName;
if (CleanFilter.StartsWith(TEXT("E")))
{
CleanFilter = CleanFilter.Mid(1);
}
if (Enum->GetName() == FilterName || Enum->GetName() == CleanFilter)
{
bMatch = true;
break;
}
}
if (!bMatch) continue;
}
FoundEnums.Add(Enum);
}
// Build list of found types
TArray<TSharedPtr<FJsonValue>> TypesFoundArr;
for (UScriptStruct* S : FoundStructs)
{
TSharedRef<FJsonObject> TJ = MakeShared<FJsonObject>();
TJ->SetStringField(TEXT("name"), S->GetName());
TJ->SetStringField(TEXT("kind"), TEXT("struct"));
TJ->SetStringField(TEXT("package"), S->GetOutermost()->GetName());
TypesFoundArr.Add(MakeShared<FJsonValueObject>(TJ));
}
for (UEnum* E : FoundEnums)
{
TSharedRef<FJsonObject> TJ = MakeShared<FJsonObject>();
TJ->SetStringField(TEXT("name"), E->GetName());
TJ->SetStringField(TEXT("kind"), TEXT("enum"));
TJ->SetStringField(TEXT("package"), E->GetOutermost()->GetName());
TypesFoundArr.Add(MakeShared<FJsonValueObject>(TJ));
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Found %d structs and %d enums in module '%s'"),
FoundStructs.Num(), FoundEnums.Num(), *ModuleName);
// Build a set of type names for fast lookup
TSet<FString> TypeNameSet;
for (UScriptStruct* S : FoundStructs) TypeNameSet.Add(S->GetName());
for (UEnum* E : FoundEnums) TypeNameSet.Add(E->GetName());
// Scan all blueprints for references
struct FBlueprintImpact
{
FString Name;
FString Path;
int32 BreakNodes = 0;
int32 MakeNodes = 0;
int32 Variables = 0;
int32 FunctionParams = 0;
int32 ConnectionsAtRisk = 0;
FString Risk;
};
TArray<FBlueprintImpact> AffectedBlueprints;
int32 TotalBreakMakeNodes = 0;
int32 TotalConnectionsAtRisk = 0;
for (const FAssetData& Asset : UMCPAssetFinder::GetBlueprintAssets())
{
FString LoadError;
UBlueprint* BP = UMCPAssetFinder::LoadBlueprintByName(Asset.AssetName.ToString(), LoadError);
if (!BP) continue;
FBlueprintImpact Impact;
Impact.Name = BP->GetName();
Impact.Path = BP->GetPathName();
// Scan graphs for Break/Make struct nodes
TArray<UEdGraph*> AllGraphs;
BP->GetAllGraphs(AllGraphs);
for (UEdGraph* Graph : AllGraphs)
{
if (!Graph) continue;
for (UEdGraphNode* Node : Graph->Nodes)
{
if (!Node) continue;
if (UK2Node_BreakStruct* BreakNode = Cast<UK2Node_BreakStruct>(Node))
{
if (BreakNode->StructType && TypeNameSet.Contains(BreakNode->StructType->GetName()))
{
Impact.BreakNodes++;
// Count data connections at risk
for (UEdGraphPin* Pin : Node->Pins)
{
if (!Pin || Pin->bHidden) continue;
if (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec) continue;
Impact.ConnectionsAtRisk += Pin->LinkedTo.Num();
}
}
}
else if (UK2Node_MakeStruct* MakeNode = Cast<UK2Node_MakeStruct>(Node))
{
if (MakeNode->StructType && TypeNameSet.Contains(MakeNode->StructType->GetName()))
{
Impact.MakeNodes++;
for (UEdGraphPin* Pin : Node->Pins)
{
if (!Pin || Pin->bHidden) continue;
if (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec) continue;
Impact.ConnectionsAtRisk += Pin->LinkedTo.Num();
}
}
}
}
}
// Scan variables
for (const FBPVariableDescription& Var : BP->NewVariables)
{
if (Var.VarType.PinSubCategoryObject.IsValid())
{
FString SubObjName = Var.VarType.PinSubCategoryObject->GetName();
if (TypeNameSet.Contains(SubObjName))
{
Impact.Variables++;
}
}
}
// Scan function parameters
for (UEdGraph* Graph : BP->FunctionGraphs)
{
if (!Graph) continue;
for (UEdGraphNode* Node : Graph->Nodes)
{
UK2Node_FunctionEntry* FuncEntry = Cast<UK2Node_FunctionEntry>(Node);
if (!FuncEntry) continue;
for (const TSharedPtr<FUserPinInfo>& PinInfo : FuncEntry->UserDefinedPins)
{
if (!PinInfo.IsValid()) continue;
if (PinInfo->PinType.PinSubCategoryObject.IsValid())
{
FString SubObjName = PinInfo->PinType.PinSubCategoryObject->GetName();
if (TypeNameSet.Contains(SubObjName))
{
Impact.FunctionParams++;
}
}
}
}
}
// Only include if this BP is affected
if (Impact.BreakNodes > 0 || Impact.MakeNodes > 0 || Impact.Variables > 0 || Impact.FunctionParams > 0)
{
// Classify risk
if (Impact.BreakNodes > 0 || Impact.MakeNodes > 0)
{
Impact.Risk = TEXT("HIGH");
}
else if (Impact.Variables > 0)
{
Impact.Risk = TEXT("MEDIUM");
}
else
{
Impact.Risk = TEXT("LOW");
}
TotalBreakMakeNodes += Impact.BreakNodes + Impact.MakeNodes;
TotalConnectionsAtRisk += Impact.ConnectionsAtRisk;
AffectedBlueprints.Add(Impact);
}
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Rebuild impact analysis complete — %d affected blueprints, %d Break/Make nodes, %d connections at risk"),
AffectedBlueprints.Num(), TotalBreakMakeNodes, TotalConnectionsAtRisk);
// Build response
Result->SetStringField(TEXT("moduleName"), ModuleName);
Result->SetArrayField(TEXT("typesFound"), TypesFoundArr);
TArray<TSharedPtr<FJsonValue>> AffectedArr;
for (const FBlueprintImpact& Impact : AffectedBlueprints)
{
TSharedRef<FJsonObject> BJ = MakeShared<FJsonObject>();
BJ->SetStringField(TEXT("name"), Impact.Name);
BJ->SetStringField(TEXT("path"), Impact.Path);
BJ->SetNumberField(TEXT("breakNodes"), Impact.BreakNodes);
BJ->SetNumberField(TEXT("makeNodes"), Impact.MakeNodes);
BJ->SetNumberField(TEXT("variables"), Impact.Variables);
BJ->SetNumberField(TEXT("functionParams"), Impact.FunctionParams);
BJ->SetNumberField(TEXT("connectionsAtRisk"), Impact.ConnectionsAtRisk);
BJ->SetStringField(TEXT("risk"), Impact.Risk);
AffectedArr.Add(MakeShared<FJsonValueObject>(BJ));
}
Result->SetArrayField(TEXT("affectedBlueprints"), AffectedArr);
TSharedRef<FJsonObject> Summary = MakeShared<FJsonObject>();
Summary->SetNumberField(TEXT("totalBlueprints"), AffectedBlueprints.Num());
Summary->SetNumberField(TEXT("totalBreakMakeNodes"), TotalBreakMakeNodes);
Summary->SetNumberField(TEXT("totalConnectionsAtRisk"), TotalConnectionsAtRisk);
Result->SetObjectField(TEXT("summary"), Summary);
}