Files
integration/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_Graphs.h
2026-03-08 01:47:15 -05:00

596 lines
19 KiB
C++

#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraphSchema_K2.h"
#include "K2Node_CustomEvent.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "UObject/UObjectIterator.h"
#include "MCPHandlers_Graphs.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(ToolName="reparent_blueprint"))
class UMCPHandler_ReparentBlueprint : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Name of the new parent class (C++ class name or Blueprint name)"))
FString NewParentClass;
virtual FString GetDescription() const override
{
return TEXT("Change a Blueprint's parent class. Accepts C++ class names or Blueprint names.");
}
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
{
// Load Blueprint
MCPAssets<UBlueprint> Assets;
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
UBlueprint* BP = Assets.Object();
FString OldParentName = BP->ParentClass ? BP->ParentClass->GetName() : TEXT("None");
// Find the new parent class
// Try C++ class first (e.g. "WebUIHUD" finds /Script/ModuleName.WebUIHUD)
UClass* NewParentClassObj = nullptr;
// Search across all packages for native classes
for (TObjectIterator<UClass> It; It; ++It)
{
if (It->GetName() == NewParentClass)
{
NewParentClassObj = *It;
break;
}
}
// If not found as C++ class, try loading as a Blueprint asset
if (!NewParentClassObj)
{
MCPAssets<UBlueprint> ParentAssets;
if (!ParentAssets.Exact(NewParentClass).AllContent().Errors(Result).ETwo().Load()) return;
if (!ParentAssets.Objects().IsEmpty())
{
if (ParentAssets.Object()->GeneratedClass)
NewParentClassObj = ParentAssets.Object()->GeneratedClass;
}
}
if (!NewParentClassObj)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Could not find class '%s'. Provide a C++ class name (e.g. 'WebUIHUD') or Blueprint name."),
*NewParentClass));
}
// Validate: new parent must be compatible
if (BP->ParentClass && !NewParentClassObj->IsChildOf(BP->ParentClass->GetSuperClass()) &&
BP->ParentClass != NewParentClassObj)
{
// Just warn, don't block — the user may intentionally reparent to a sibling
UE_LOG(LogTemp, Warning,
TEXT("BlueprintMCP: Reparenting '%s' from '%s' to '%s' — classes are not in a direct hierarchy"),
*Blueprint, *OldParentName, *NewParentClassObj->GetName());
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Reparenting '%s' from '%s' to '%s'"),
*Blueprint, *OldParentName, *NewParentClassObj->GetName());
// Perform reparent
BP->ParentClass = NewParentClassObj;
// Refresh all nodes to pick up new parent's functions/variables
FBlueprintEditorUtils::RefreshAllNodes(BP);
// Compile
FKismetEditorUtilities::CompileBlueprint(BP);
// Save
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
FString NewParentActualName = NewParentClassObj->GetName();
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Reparent complete, save %s"),
bSaved ? TEXT("succeeded") : TEXT("failed"));
Result->SetStringField(TEXT("oldParentClass"), OldParentName);
Result->SetStringField(TEXT("newParentClass"), NewParentActualName);
Result->SetBoolField(TEXT("saved"), bSaved);
}
};
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(ToolName="create_blueprint_asset"))
class UMCPHandler_CreateBlueprint : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="New Blueprint asset name"))
FString Blueprint;
UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)"))
FString PackagePath;
UPROPERTY(meta=(Description="Parent class name (C++ class name or Blueprint name)"))
FString ParentClass;
UPROPERTY(meta=(Optional, Description="Blueprint type: Normal, Interface, FunctionLibrary, or MacroLibrary (default: Normal)"))
FString BlueprintType;
virtual FString GetDescription() const override
{
return TEXT("Create a new Blueprint asset with a specified parent class and type.");
}
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
{
// Validate packagePath starts with /Game
if (!PackagePath.StartsWith(TEXT("/Game")))
{
return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'"));
}
// Check if asset already exists
FString FullAssetPath = PackagePath / Blueprint;
MCPAssets<UBlueprint> ExistCheck;
if (!ExistCheck.Exact(Blueprint).Errors(Result).EAny().Info()) return;
// Resolve parent class — try C++ class first, then Blueprint
UClass* ParentClassObj = nullptr;
for (TObjectIterator<UClass> It; It; ++It)
{
if (It->GetName() == ParentClass)
{
ParentClassObj = *It;
break;
}
}
if (!ParentClassObj)
{
MCPAssets<UBlueprint> ParentAssets;
if (!ParentAssets.Exact(ParentClass).AllContent().Errors(Result).ETwo().Load()) return;
if (!ParentAssets.Objects().IsEmpty())
{
if (ParentAssets.Object()->GeneratedClass)
ParentClassObj = ParentAssets.Object()->GeneratedClass;
}
}
if (!ParentClassObj)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Could not find parent class '%s'. Provide a C++ class name (e.g. 'Actor', 'Pawn') or Blueprint name."),
*ParentClass));
}
// Map blueprintType string to EBlueprintType
EBlueprintType BlueprintTypeEnum = BPTYPE_Normal;
if (!BlueprintType.IsEmpty())
{
if (BlueprintType == TEXT("Interface"))
{
BlueprintTypeEnum = BPTYPE_Interface;
}
else if (BlueprintType == TEXT("FunctionLibrary"))
{
BlueprintTypeEnum = BPTYPE_FunctionLibrary;
}
else if (BlueprintType == TEXT("MacroLibrary"))
{
BlueprintTypeEnum = BPTYPE_MacroLibrary;
}
else if (BlueprintType != TEXT("Normal"))
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Invalid blueprintType '%s'. Valid values: Normal, Interface, FunctionLibrary, MacroLibrary"),
*BlueprintType));
}
}
// For Interface type, parent must be UInterface
if ((BlueprintTypeEnum == BPTYPE_Interface) && !ParentClassObj->IsChildOf(UInterface::StaticClass()))
{
// Use the engine's standard BlueprintInterface parent
ParentClassObj = UInterface::StaticClass();
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating Blueprint '%s' in '%s' with parent '%s' (type=%s)"),
*Blueprint, *PackagePath, *ParentClassObj->GetName(), *BlueprintType);
// Create the package
FString FullPackagePath = PackagePath / Blueprint;
UPackage* Package = CreatePackage(*FullPackagePath);
if (!Package)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create package at '%s'"), *FullPackagePath));
}
// Create the Blueprint
UBlueprint* NewBP = FKismetEditorUtilities::CreateBlueprint(
ParentClassObj,
Package,
FName(*Blueprint),
BlueprintTypeEnum,
UBlueprint::StaticClass(),
UBlueprintGeneratedClass::StaticClass()
);
if (!NewBP)
{
return MCPUtils::MakeErrorJson(Result, TEXT("FKismetEditorUtilities::CreateBlueprint returned null"));
}
// Compile
FKismetEditorUtilities::CompileBlueprint(NewBP);
// Save
bool bSaved = MCPUtils::SaveBlueprintPackage(NewBP);
// Collect graph names
TArray<TSharedPtr<FJsonValue>> GraphNames;
for (UEdGraph* Graph : NewBP->UbergraphPages)
{
GraphNames.Add(MakeShared<FJsonValueString>(Graph->GetName()));
}
for (UEdGraph* Graph : NewBP->FunctionGraphs)
{
GraphNames.Add(MakeShared<FJsonValueString>(Graph->GetName()));
}
for (UEdGraph* Graph : NewBP->MacroGraphs)
{
GraphNames.Add(MakeShared<FJsonValueString>(Graph->GetName()));
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Blueprint '%s' with %d graphs (saved: %s)"),
*Blueprint, GraphNames.Num(), bSaved ? TEXT("true") : TEXT("false"));
Result->SetStringField(TEXT("assetPath"), FullAssetPath);
Result->SetStringField(TEXT("parentClass"), ParentClassObj->GetName());
Result->SetBoolField(TEXT("saved"), bSaved);
Result->SetArrayField(TEXT("graphs"), GraphNames);
}
};
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(ToolName="create_blueprint_graph"))
class UMCPHandler_CreateGraph : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Name for the new graph"))
FString Graph;
UPROPERTY(meta=(Description="Type of graph: function, macro, or customEvent"))
FString GraphType;
virtual FString GetDescription() const override
{
return TEXT("Create a new function, macro, or custom event graph in a Blueprint.");
}
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
{
if (GraphType != TEXT("function") && GraphType != TEXT("macro") && GraphType != TEXT("customEvent"))
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Invalid graphType '%s'. Valid values: function, macro, customEvent"), *GraphType));
}
// Load Blueprint
MCPAssets<UBlueprint> Assets;
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
UBlueprint* BP = Assets.Object();
// Check graph name uniqueness
TArray<UEdGraph*> AllGraphs;
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));
}
}
// Also check for existing custom events with the same name
if (GraphType == TEXT("customEvent"))
{
for (UEdGraph* ExistingGraph : AllGraphs)
{
if (!ExistingGraph) continue;
for (UEdGraphNode* Node : ExistingGraph->Nodes)
{
if (UK2Node_CustomEvent* CE = Cast<UK2Node_CustomEvent>(Node))
{
if (CE->CustomFunctionName == FName(*Graph))
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("A custom event named '%s' already exists in Blueprint '%s'"), *Graph, *Blueprint));
}
}
}
}
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating %s graph '%s' in Blueprint '%s'"),
*GraphType, *Graph, *Blueprint);
FString CreatedNodeId;
if (GraphType == TEXT("function"))
{
UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(BP, FName(*Graph),
UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass());
if (!NewGraph)
{
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create function graph"));
}
FBlueprintEditorUtils::AddFunctionGraph(BP, NewGraph, /*bIsUserCreated=*/true, /*SignatureFromObject=*/static_cast<UClass*>(nullptr));
}
else if (GraphType == TEXT("macro"))
{
UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(BP, FName(*Graph),
UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass());
if (!NewGraph)
{
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create macro graph"));
}
FBlueprintEditorUtils::AddMacroGraph(BP, NewGraph, /*bIsUserCreated=*/true, /*SignatureFromClass=*/nullptr);
}
else // customEvent
{
// Find the EventGraph (first UbergraphPage)
UEdGraph* EventGraph = nullptr;
if (BP->UbergraphPages.Num() > 0)
{
EventGraph = BP->UbergraphPages[0];
}
if (!EventGraph)
{
return MCPUtils::MakeErrorJson(Result, TEXT("Blueprint has no EventGraph to add a custom event to"));
}
// Create a custom event node in the EventGraph
UK2Node_CustomEvent* NewEvent = NewObject<UK2Node_CustomEvent>(EventGraph);
NewEvent->CustomFunctionName = FName(*Graph);
NewEvent->bIsEditable = true;
EventGraph->AddNode(NewEvent, /*bFromUI=*/false, /*bSelectNewNode=*/false);
NewEvent->CreateNewGuid();
NewEvent->PostPlacedNewNode();
NewEvent->AllocateDefaultPins();
CreatedNodeId = NewEvent->NodeGuid.ToString();
}
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created %s graph '%s' in '%s' (saved: %s)"),
*GraphType, *Graph, *Blueprint, bSaved ? TEXT("true") : TEXT("false"));
Result->SetBoolField(TEXT("saved"), bSaved);
if (!CreatedNodeId.IsEmpty())
{
Result->SetStringField(TEXT("nodeId"), CreatedNodeId);
}
}
};
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(ToolName="delete_blueprint_graph"))
class UMCPHandler_DeleteGraph : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Name of the graph to delete"))
FString Graph;
virtual FString GetDescription() const override
{
return TEXT("Delete a function or macro graph from a Blueprint. Cannot delete EventGraph pages.");
}
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
{
MCPAssets<UBlueprint> Assets;
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
UBlueprint* BP = Assets.Object();
// Find the graph
UEdGraph* TargetGraph = nullptr;
FString GraphType;
for (UEdGraph* CandidateGraph : BP->FunctionGraphs)
{
if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase))
{
TargetGraph = CandidateGraph;
GraphType = TEXT("function");
break;
}
}
if (!TargetGraph)
{
for (UEdGraph* CandidateGraph : BP->MacroGraphs)
{
if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase))
{
TargetGraph = CandidateGraph;
GraphType = TEXT("macro");
break;
}
}
}
// Check if it's an UbergraphPage (EventGraph) — disallow deletion
if (!TargetGraph)
{
for (UEdGraph* CandidateGraph : BP->UbergraphPages)
{
if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase))
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Cannot delete UbergraphPage '%s'. EventGraph and other Ubergraph pages cannot be deleted."),
*Graph));
}
}
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found in Blueprint '%s'"), *Graph, *Blueprint));
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleting %s graph '%s' from Blueprint '%s'"),
*GraphType, *Graph, *Blueprint);
// Count nodes for reporting
int32 NodeCount = TargetGraph->Nodes.Num();
// Remove the graph
FBlueprintEditorUtils::RemoveGraph(BP, TargetGraph, EGraphRemoveFlags::Default);
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleted graph '%s' (%d nodes), save %s"),
*Graph, NodeCount, bSaved ? TEXT("true") : TEXT("false"));
Result->SetStringField(TEXT("graphType"), GraphType);
Result->SetNumberField(TEXT("nodeCount"), NodeCount);
Result->SetBoolField(TEXT("saved"), bSaved);
}
};
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(ToolName="rename_blueprint_graph"))
class UMCPHandler_RenameGraph : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Current name of the graph to rename"))
FString Graph;
UPROPERTY(meta=(Description="New name for the graph"))
FString NewName;
virtual FString GetDescription() const override
{
return TEXT("Rename a function or macro graph in a Blueprint. Cannot rename EventGraph pages.");
}
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
{
MCPAssets<UBlueprint> Assets;
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
UBlueprint* BP = Assets.Object();
// Check if it's an UbergraphPage — disallow rename
for (UEdGraph* CandidateGraph : BP->UbergraphPages)
{
if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase))
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Cannot rename UbergraphPage '%s'. EventGraph and other Ubergraph pages cannot be renamed."),
*Graph));
}
}
// Find the graph in FunctionGraphs or MacroGraphs
UEdGraph* TargetGraph = nullptr;
FString GraphType;
for (UEdGraph* CandidateGraph : BP->FunctionGraphs)
{
if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase))
{
TargetGraph = CandidateGraph;
GraphType = TEXT("function");
break;
}
}
if (!TargetGraph)
{
for (UEdGraph* CandidateGraph : BP->MacroGraphs)
{
if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase))
{
TargetGraph = CandidateGraph;
GraphType = TEXT("macro");
break;
}
}
}
if (!TargetGraph)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found in Blueprint '%s'"), *Graph, *Blueprint));
}
// Check for name collision
TArray<UEdGraph*> AllGraphs;
BP->GetAllGraphs(AllGraphs);
for (UEdGraph* Existing : AllGraphs)
{
if (Existing && Existing != TargetGraph && Existing->GetName().Equals(NewName, ESearchCase::IgnoreCase))
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("A graph named '%s' already exists in Blueprint '%s'"), *NewName, *Blueprint));
}
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Renaming %s graph '%s' to '%s' in Blueprint '%s'"),
*GraphType, *Graph, *NewName, *Blueprint);
FBlueprintEditorUtils::RenameGraph(TargetGraph, NewName);
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Renamed graph '%s' to '%s', save %s"),
*Graph, *NewName, bSaved ? TEXT("true") : TEXT("false"));
Result->SetStringField(TEXT("newName"), TargetGraph->GetName());
Result->SetStringField(TEXT("graphType"), GraphType);
Result->SetBoolField(TEXT("saved"), bSaved);
}
};