Refactoring more MCP handlers.

This commit is contained in:
2026-03-10 01:42:43 -04:00
parent 2a5833fe04
commit 519933d532
47 changed files with 516 additions and 689 deletions

Binary file not shown.

View File

@@ -59,7 +59,8 @@ public:
const TArray<USCS_Node*>& ExistingNodes = SCS->GetAllNodes();
for (USCS_Node* Existing : ExistingNodes)
{
if (Existing && Existing->GetVariableName().ToString().Equals(Component, ESearchCase::IgnoreCase))
if (Existing && Existing->ComponentTemplate &&
MCPUtils::Identifies(Component, Existing->ComponentTemplate))
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("A component named '%s' already exists in Blueprint '%s'"),
@@ -68,42 +69,13 @@ public:
}
// Resolve the component class by name
// Try multiple name variants: exact name, with U prefix, without U prefix
UClass* ComponentClassObj = nullptr;
TArray<FString> NamesToTry;
NamesToTry.Add(ComponentClass);
if (!ComponentClass.StartsWith(TEXT("U")))
{
NamesToTry.Add(FString::Printf(TEXT("U%s"), *ComponentClass));
}
else
{
// Also try without U prefix
NamesToTry.Add(ComponentClass.Mid(1));
}
for (TObjectIterator<UClass> It; It; ++It)
{
if (!It->IsChildOf(UActorComponent::StaticClass()))
{
continue;
}
FString ClassName = It->GetName();
for (const FString& NameToTry : NamesToTry)
{
if (ClassName.Equals(NameToTry, ESearchCase::IgnoreCase))
{
ComponentClassObj = *It;
break;
}
}
if (ComponentClassObj)
{
break;
}
if (!It->IsChildOf(UActorComponent::StaticClass())) continue;
if (!MCPUtils::Identifies(ComponentClass, *It)) continue;
ComponentClassObj = *It;
break;
}
if (!ComponentClassObj)
@@ -123,7 +95,8 @@ public:
{
for (USCS_Node* Node : ExistingNodes)
{
if (Node && Node->GetVariableName().ToString().Equals(ParentComponent, ESearchCase::IgnoreCase))
if (Node && Node->ComponentTemplate &&
MCPUtils::Identifies(ParentComponent, Node->ComponentTemplate))
{
ParentSCSNode = Node;
break;
@@ -139,7 +112,7 @@ public:
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Adding component '%s' (%s) to Blueprint '%s'"),
*Component, *ComponentClassObj->GetName(), *Blueprint);
*Component, *MCPUtils::FormatName(ComponentClassObj), *Blueprint);
// Create the SCS node
USCS_Node* NewNode = SCS->CreateNode(ComponentClassObj, FName(*Component));
@@ -147,7 +120,7 @@ public:
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Failed to create SCS node for component '%s' with class '%s'"),
*Component, *ComponentClassObj->GetName()));
*Component, *MCPUtils::FormatName(ComponentClassObj)));
}
// Add to the hierarchy
@@ -164,15 +137,15 @@ public:
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added component '%s' (%s) to '%s' (parent: %s, saved: %s)"),
*Component, *ComponentClassObj->GetName(), *Blueprint,
*Component, *MCPUtils::FormatName(ComponentClassObj), *Blueprint,
ParentSCSNode ? *ParentComponent : TEXT("(root)"),
bSaved ? TEXT("true") : TEXT("false"));
Result->SetStringField(TEXT("component"), NewNode->GetVariableName().ToString());
Result->SetStringField(TEXT("componentClass"), ComponentClassObj->GetName());
Result->SetStringField(TEXT("component"), MCPUtils::FormatName(NewNode->ComponentTemplate));
Result->SetStringField(TEXT("componentClass"), MCPUtils::FormatName(ComponentClassObj));
if (ParentSCSNode)
{
Result->SetStringField(TEXT("parentComponent"), ParentSCSNode->GetVariableName().ToString());
Result->SetStringField(TEXT("parentComponent"), MCPUtils::FormatName(ParentSCSNode->ComponentTemplate));
}
Result->SetBoolField(TEXT("saved"), bSaved);
}

View File

@@ -106,7 +106,7 @@ public:
FTopLevelAssetPath InterfacePath = InterfaceClass->GetClassPathName();
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Adding interface '%s' to Blueprint '%s'"),
*InterfaceClass->GetName(), *Blueprint);
*MCPUtils::FormatName(InterfaceClass), *Blueprint);
bool bAdded = FBlueprintEditorUtils::ImplementNewInterface(BP, InterfacePath);
if (!bAdded)
@@ -126,7 +126,7 @@ public:
{
if (Graph)
{
AddedFunctions.Add(Graph->GetName());
AddedFunctions.Add(MCPUtils::FormatName(Graph));
}
}
break;
@@ -137,9 +137,9 @@ public:
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added interface '%s' to '%s' (%d function stubs)"),
*InterfaceClass->GetName(), *Blueprint, AddedFunctions.Num());
*MCPUtils::FormatName(InterfaceClass), *Blueprint, AddedFunctions.Num());
Result->SetStringField(TEXT("interfaceName"), InterfaceClass->GetName());
Result->SetStringField(TEXT("interfaceName"), MCPUtils::FormatName(InterfaceClass));
Result->SetStringField(TEXT("interfacePath"), InterfaceClass->GetPathName());
TArray<TSharedPtr<FJsonValue>> FuncArr;

View File

@@ -62,7 +62,7 @@ public:
for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
{
UEdGraph* FEGraph = FE->GetGraph();
if (!FEGraph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) continue;
if (!MCPUtils::Identifies(FunctionName, FEGraph)) continue;
// Skip delegate signature graphs (handled in Strategy 3)
if (BP->DelegateSignatureGraphs.Contains(FEGraph)) continue;
@@ -91,7 +91,7 @@ public:
for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
{
UEdGraph* FEGraph = FE->GetGraph();
if (!FEGraph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) continue;
if (!MCPUtils::Identifies(FunctionName, FEGraph)) continue;
if (!BP->DelegateSignatureGraphs.Contains(FEGraph)) continue;
EntryNode = FE;
@@ -107,7 +107,7 @@ public:
for (UEdGraph* Graph : BP->FunctionGraphs)
{
if (Graph) AvailFuncs.Add(MakeShared<FJsonValueString>(Graph->GetName()));
if (Graph) AvailFuncs.Add(MakeShared<FJsonValueString>(MCPUtils::FormatName(Graph)));
}
// Custom events

View File

@@ -114,7 +114,7 @@ public:
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());
AffNode->SetStringField(TEXT("graph"), MCPUtils::FormatName(VG->GetGraph()));
TArray<TSharedPtr<FJsonValue>> AffPins;
for (UEdGraphPin* Pin : VG->Pins)
{
@@ -122,7 +122,7 @@ public:
{
AffPins.Add(MakeShared<FJsonValueString>(
FString::Printf(TEXT("%s (connected to %d pin(s))"),
*Pin->PinName.ToString(), Pin->LinkedTo.Num())));
*MCPUtils::FormatName(Pin), Pin->LinkedTo.Num())));
}
}
AffNode->SetArrayField(TEXT("affectedPins"), AffPins);
@@ -134,7 +134,7 @@ public:
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());
AffNode->SetStringField(TEXT("graph"), MCPUtils::FormatName(VS->GetGraph()));
TArray<TSharedPtr<FJsonValue>> AffPins;
for (UEdGraphPin* Pin : VS->Pins)
{
@@ -142,7 +142,7 @@ public:
{
AffPins.Add(MakeShared<FJsonValueString>(
FString::Printf(TEXT("%s (connected to %d pin(s))"),
*Pin->PinName.ToString(), Pin->LinkedTo.Num())));
*MCPUtils::FormatName(Pin), Pin->LinkedTo.Num())));
}
}
AffNode->SetArrayField(TEXT("affectedPins"), AffPins);

View File

@@ -63,7 +63,7 @@ public:
// Strategy 1: Look for a K2Node_FunctionEntry in a function graph matching the name
for (UK2Node_FunctionEntry* FuncEntry : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
{
if (FuncEntry->GetGraph()->GetName().Equals(FunctionName, ESearchCase::IgnoreCase))
if (MCPUtils::Identifies(FunctionName, FuncEntry->GetGraph()))
{
EntryNode = FuncEntry;
FoundNodeType = TEXT("FunctionEntry");
@@ -92,7 +92,7 @@ public:
for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
{
Available.Add(MakeShared<FJsonValueString>(
FString::Printf(TEXT("function:%s"), *FE->GetGraph()->GetName())));
FString::Printf(TEXT("function:%s"), *MCPUtils::FormatName(FE->GetGraph()))));
}
for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
{
@@ -147,16 +147,16 @@ public:
TArray<TSharedPtr<FJsonValue>> AffectedPins;
for (UEdGraphPin* Pin : EntryNode->Pins)
{
if (Pin && Pin->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase) && Pin->LinkedTo.Num() > 0)
if (Pin && MCPUtils::Identifies(ParamName, Pin) && Pin->LinkedTo.Num() > 0)
{
for (UEdGraphPin* Linked : Pin->LinkedTo)
{
if (Linked && Linked->GetOwningNode())
{
TSharedRef<FJsonObject> AffPin = MakeShared<FJsonObject>();
AffPin->SetStringField(TEXT("pinName"), Pin->PinName.ToString());
AffPin->SetStringField(TEXT("pinName"), MCPUtils::FormatName(Pin));
AffPin->SetStringField(TEXT("connectedToNode"), Linked->GetOwningNode()->NodeGuid.ToString());
AffPin->SetStringField(TEXT("connectedToPin"), Linked->PinName.ToString());
AffPin->SetStringField(TEXT("connectedToPin"), MCPUtils::FormatName(Linked));
AffPin->SetStringField(TEXT("currentType"), Pin->PinType.PinCategory.ToString());
if (Pin->PinType.PinSubCategoryObject.IsValid())
AffPin->SetStringField(TEXT("currentSubtype"), Pin->PinType.PinSubCategoryObject->GetName());

View File

@@ -99,7 +99,7 @@ public:
if (!BreakNode && !MakeNode)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' is not a BreakStruct or MakeStruct node (class: %s)"),
*Node, *FoundNode->GetClass()->GetName()));
*Node, *MCPUtils::FormatName(FoundNode->GetClass())));
}
// Find the new struct type
@@ -267,7 +267,7 @@ public:
TSharedPtr<FJsonObject> UpdatedNodeState = MCPUtils::SerializeNode(FoundNode);
Result->SetStringField(TEXT("newStructType"), NewStruct->GetName());
Result->SetStringField(TEXT("nodeClass"), FoundNode->GetClass()->GetName());
Result->SetStringField(TEXT("nodeClass"), MCPUtils::FormatName(FoundNode->GetClass()));
Result->SetNumberField(TEXT("reconnected"), Reconnected);
Result->SetNumberField(TEXT("failed"), Failed);
Result->SetArrayField(TEXT("reconnectDetails"), ReconnectDetails);

View File

@@ -67,10 +67,10 @@ public:
if (Node->bHasCompilerMessage)
{
TSharedRef<FJsonObject> Msg = MakeShared<FJsonObject>();
Msg->SetStringField(TEXT("graph"), Node->GetGraph()->GetName());
Msg->SetStringField(TEXT("graph"), MCPUtils::FormatName(Node->GetGraph()));
Msg->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
Msg->SetStringField(TEXT("nodeTitle"), Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString());
Msg->SetStringField(TEXT("nodeClass"), Node->GetClass()->GetName());
Msg->SetStringField(TEXT("nodeTitle"), MCPUtils::FormatName(Node));
Msg->SetStringField(TEXT("nodeClass"), MCPUtils::FormatName(Node->GetClass()));
Msg->SetStringField(TEXT("message"), Node->ErrorMsg);
if (Node->ErrorType == EMessageSeverity::Error)

View File

@@ -1,134 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "Engine/World.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "UMCPHandler_ConnectBlueprintPins.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FConnectPinsEntry
{
GENERATED_BODY()
UPROPERTY()
FString SourceNode;
UPROPERTY()
FString SourcePinName;
UPROPERTY()
FString TargetNode;
UPROPERTY()
FString TargetPinName;
};
UCLASS()
class UMCPHandler_ConnectBlueprintPins : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Array of {sourceNode, sourcePinName, targetNode, targetPinName} objects"))
FMCPJsonArray Connections;
virtual FString GetDescription() const override
{
return TEXT("Connect pins between nodes in a Blueprint graph.");
}
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();
TArray<TSharedPtr<FJsonValue>> Results;
int32 SuccessCount = 0;
for (const TSharedPtr<FJsonValue>& ConnVal : Connections.Array)
{
TSharedRef<FJsonObject> EntryResult = MakeShared<FJsonObject>();
Results.Add(MakeShared<FJsonValueObject>(EntryResult));
FConnectPinsEntry Entry;
if (!MCPUtils::PopulateFromJson(FConnectPinsEntry::StaticStruct(), &Entry, ConnVal, &*EntryResult)) continue;
UEdGraph* SourceGraph = nullptr;
UEdGraphNode* SourceNode = MCPUtils::FindNodeByGuid(BP, Entry.SourceNode, &SourceGraph);
if (!SourceNode)
{
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Source node '%s' not found"), *Entry.SourceNode));
continue;
}
UEdGraphNode* TargetNode = MCPUtils::FindNodeByGuid(BP, Entry.TargetNode);
if (!TargetNode)
{
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target node '%s' not found"), *Entry.TargetNode));
continue;
}
UEdGraphPin* SourcePin = SourceNode->FindPin(FName(*Entry.SourcePinName));
if (!SourcePin)
{
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Source pin '%s' not found on node '%s'"), *Entry.SourcePinName, *Entry.SourceNode));
continue;
}
UEdGraphPin* TargetPin = TargetNode->FindPin(FName(*Entry.TargetPinName));
if (!TargetPin)
{
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), *Entry.TargetPinName, *Entry.TargetNode));
continue;
}
const UEdGraphSchema* Schema = SourceGraph->GetSchema();
if (!Schema)
{
EntryResult->SetStringField(TEXT("error"), TEXT("Graph schema not found"));
continue;
}
bool bConnected = Schema->TryCreateConnection(SourcePin, TargetPin);
if (!bConnected)
{
EntryResult->SetStringField(TEXT("error"), FString::Printf(
TEXT("Cannot connect %s (%s) to %s (%s) — types are incompatible"),
*Entry.SourcePinName, *SourcePin->PinType.PinCategory.ToString(),
*Entry.TargetPinName, *TargetPin->PinType.PinCategory.ToString()));
continue;
}
SuccessCount++;
}
if (SuccessCount > 0)
{
FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: ConnectPins — %d/%d succeeded in '%s'"),
SuccessCount, Connections.Array.Num(), *Blueprint);
Result->SetNumberField(TEXT("successCount"), SuccessCount);
Result->SetNumberField(TEXT("totalCount"), Connections.Array.Num());
Result->SetArrayField(TEXT("results"), Results);
}
};

View File

@@ -0,0 +1,102 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "UMCPHandler_ConnectPins.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FConnectPinsEntry
{
GENERATED_BODY()
UPROPERTY()
FString SourcePin;
UPROPERTY()
FString TargetPin;
};
UCLASS()
class UMCPHandler_ConnectPins : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Array of {sourcePin, targetPin} objects. Each pin is a path like node:MyNode,pin:Output"))
FMCPJsonArray Connections;
virtual FString GetDescription() const override
{
return TEXT("Connect pins between nodes in a Blueprint graph.");
}
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
{
MCPFetcher F(Result);
UBlueprint* BP = F.Walk(Blueprint).Cast<UBlueprint>();
if (!BP) return;
TArray<TSharedPtr<FJsonValue>> Results;
int32 SuccessCount = 0;
for (const TSharedPtr<FJsonValue>& ConnVal : Connections.Array)
{
TSharedRef<FJsonObject> EntryResult = MakeShared<FJsonObject>();
Results.Add(MakeShared<FJsonValueObject>(EntryResult));
FConnectPinsEntry Entry;
if (!MCPUtils::PopulateFromJson(FConnectPinsEntry::StaticStruct(), &Entry, ConnVal, &*EntryResult)) continue;
MCPFetcher FS(EntryResult, BP);
UEdGraphPin* SourcePin = FS.Walk(Entry.SourcePin).Cast<UEdGraphPin>();
if (!SourcePin) continue;
MCPFetcher FT(EntryResult, BP);
UEdGraphPin* TargetPin = FT.Walk(Entry.TargetPin).Cast<UEdGraphPin>();
if (!TargetPin) continue;
const UEdGraphSchema* Schema = SourcePin->GetOwningNode()->GetGraph()->GetSchema();
const FPinConnectionResponse Response = Schema->CanCreateConnection(SourcePin, TargetPin);
if (Response.Response == CONNECT_RESPONSE_DISALLOW)
{
EntryResult->SetStringField(TEXT("error"), FString::Printf(
TEXT("Cannot connect %s.%s to %s.%s: %s"),
*MCPUtils::FormatName(SourcePin->GetOwningNode()), *MCPUtils::FormatName(SourcePin),
*MCPUtils::FormatName(TargetPin->GetOwningNode()), *MCPUtils::FormatName(TargetPin),
*Response.Message.ToString()));
continue;
}
Schema->TryCreateConnection(SourcePin, TargetPin);
SuccessCount++;
}
if (SuccessCount > 0)
{
FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: ConnectPins — %d/%d succeeded in '%s'"),
SuccessCount, Connections.Array.Num(), *Blueprint);
Result->SetNumberField(TEXT("successCount"), SuccessCount);
Result->SetNumberField(TEXT("totalCount"), Connections.Array.Num());
Result->SetArrayField(TEXT("results"), Results);
}
};

View File

@@ -127,7 +127,7 @@ public:
Result->SetStringField(TEXT("assetPath"), FullAssetPath);
Result->SetStringField(TEXT("targetSkeleton"), SkeletonObj->GetName());
Result->SetStringField(TEXT("parentClass"), ParentClassObj->GetName());
Result->SetStringField(TEXT("parentClass"), MCPUtils::FormatName(ParentClassObj));
Result->SetBoolField(TEXT("saved"), bSaved);
Result->SetArrayField(TEXT("graphs"), GraphNames);
}

View File

@@ -100,7 +100,7 @@ public:
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating Blueprint '%s' in '%s' with parent '%s' (type=%s)"),
*Blueprint, *PackagePath, *ParentClassObj->GetName(), *BlueprintType);
*Blueprint, *PackagePath, *MCPUtils::FormatName(ParentClassObj), *BlueprintType);
// Create the package
FString FullPackagePath = PackagePath / Blueprint;
@@ -139,7 +139,7 @@ public:
*Blueprint, GraphNames.Num(), bSaved ? TEXT("true") : TEXT("false"));
Result->SetStringField(TEXT("assetPath"), FullAssetPath);
Result->SetStringField(TEXT("parentClass"), ParentClassObj->GetName());
Result->SetStringField(TEXT("parentClass"), MCPUtils::FormatName(ParentClassObj));
Result->SetBoolField(TEXT("saved"), bSaved);
Result->SetArrayField(TEXT("graphs"), GraphNames);
}

View File

@@ -48,7 +48,7 @@ public:
for (UEdGraph* CandidateGraph : BP->FunctionGraphs)
{
if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase))
if (CandidateGraph && MCPUtils::Identifies(Graph, CandidateGraph))
{
TargetGraph = CandidateGraph;
GraphType = TEXT("function");
@@ -59,7 +59,7 @@ public:
{
for (UEdGraph* CandidateGraph : BP->MacroGraphs)
{
if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase))
if (CandidateGraph && MCPUtils::Identifies(Graph, CandidateGraph))
{
TargetGraph = CandidateGraph;
GraphType = TEXT("macro");
@@ -73,7 +73,7 @@ public:
{
for (UEdGraph* CandidateGraph : BP->UbergraphPages)
{
if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase))
if (CandidateGraph && MCPUtils::Identifies(Graph, CandidateGraph))
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Cannot delete UbergraphPage '%s'. EventGraph and other Ubergraph pages cannot be deleted."),

View File

@@ -131,8 +131,8 @@ public:
}
// Capture info before deletion
FString DeletedNodeTitle = TargetMatNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
FString DeletedExprClass = TargetMatNode->MaterialExpression->GetClass()->GetName();
FString DeletedNodeTitle = MCPUtils::FormatName(TargetMatNode);
FString DeletedExprClass = MCPUtils::FormatName(TargetMatNode->MaterialExpression->GetClass());
if (DryRun)
{

View File

@@ -2,52 +2,14 @@
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPServer.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "Materials/Material.h"
#include "Materials/MaterialInstanceConstant.h"
#include "Materials/MaterialFunction.h"
#include "Engine/World.h"
#include "Engine/LevelScriptBlueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "EdGraphSchema_K2.h"
#include "K2Node.h"
#include "K2Node_CallFunction.h"
#include "K2Node_Event.h"
#include "K2Node_CustomEvent.h"
#include "K2Node_FunctionEntry.h"
#include "K2Node_EditablePinBase.h"
#include "K2Node_VariableGet.h"
#include "K2Node_VariableSet.h"
#include "K2Node_BreakStruct.h"
#include "K2Node_MakeStruct.h"
#include "K2Node_DynamicCast.h"
#include "K2Node_CallParentFunction.h"
#include "K2Node_IfThenElse.h"
#include "K2Node_ExecutionSequence.h"
#include "K2Node_MacroInstance.h"
#include "K2Node_SpawnActorFromClass.h"
#include "K2Node_Select.h"
#include "K2Node_Knot.h"
#include "EdGraphNode_Comment.h"
#include "GameFramework/Actor.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonWriter.h"
#include "Serialization/JsonSerializer.h"
#include "UObject/SavePackage.h"
#include "UObject/UObjectIterator.h"
#include "Misc/PackageName.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "AssetToolsModule.h"
#include "IAssetTools.h"
#include "BlueprintNodeSpawner.h"
#include "UMCPHandler_DeleteNodeFromGraph.generated.h"
@@ -61,10 +23,7 @@ class UMCPHandler_DeleteNodeFromGraph : public UObject, public IMCPHandler
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Node GUID"))
UPROPERTY(meta=(Description="Path to the node, e.g. /Game/Foo,node:MyNode"))
FString Node;
virtual FString GetDescription() const override
@@ -76,24 +35,16 @@ public:
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();
MCPFetcher F(Result);
UEdGraphNode* FoundNode = F.Walk(Node).Cast<UEdGraphNode>();
if (!FoundNode) return;
UEdGraph* Graph = nullptr;
UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node, &Graph);
if (!FoundNode)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *Node));
}
if (!Graph)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph not found for node '%s'"), *Node));
}
UEdGraph* Graph = FoundNode->GetGraph();
UBlueprint* BP = FBlueprintEditorUtils::FindBlueprintForNodeChecked(FoundNode);
FString NodeClass = FoundNode->GetClass()->GetName();
FString NodeTitle = FoundNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
FString GraphName = Graph->GetName();
FString NodeClass = MCPUtils::FormatName(FoundNode->GetClass());
FString NodeTitle = MCPUtils::FormatName(FoundNode);
FString GraphName = MCPUtils::FormatName(Graph);
// Protect root/entry nodes — deleting these leaves the graph in an invalid
// state with no root node, causing compiler errors that can't be fixed
@@ -121,8 +72,8 @@ public:
*NodeTitle, *GraphName));
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleting node '%s' (%s) from graph '%s' in '%s'"),
*Node, *NodeTitle, *GraphName, *Blueprint);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleting node '%s' (%s) from graph '%s'"),
*MCPUtils::FormatName(FoundNode), *NodeTitle, *GraphName);
FoundNode->BreakAllNodeLinks();
Graph->RemoveNode(FoundNode);

View File

@@ -150,7 +150,7 @@ public:
}
else
{
NodeDesc = Expr->GetClass()->GetName();
NodeDesc = MCPUtils::FormatName(Expr->GetClass());
}
// If the source node has input pins with connections, recurse
@@ -170,7 +170,7 @@ public:
else
{
// Non-material node (e.g., root, comment), just use title
NodeDesc = SourceNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
NodeDesc = MCPUtils::FormatName(SourceNode);
}
Sources.Add(NodeDesc);
@@ -201,7 +201,7 @@ public:
{
if (!Pin || Pin->Direction != EGPD_Input) continue;
FString PinName = Pin->PinName.ToString();
FString PinName = MCPUtils::FormatName(Pin);
FString Description;
if (Pin->LinkedTo.Num() == 0)

View File

@@ -117,13 +117,13 @@ public:
for (UEdGraphNode* N : GA->Nodes)
{
if (!N) continue;
FString Title = N->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
FString Title = MCPUtils::FormatName(N);
NodesA.FindOrAdd(Title).Add(N);
}
for (UEdGraphNode* N : GB->Nodes)
{
if (!N) continue;
FString Title = N->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
FString Title = MCPUtils::FormatName(N);
NodesB.FindOrAdd(Title).Add(N);
}
@@ -141,7 +141,7 @@ public:
{
TSharedRef<FJsonObject> NObj = MakeShared<FJsonObject>();
NObj->SetStringField(TEXT("title"), Pair.Key);
NObj->SetStringField(TEXT("class"), Pair.Value[0]->GetClass()->GetName());
NObj->SetStringField(TEXT("class"), MCPUtils::FormatName(Pair.Value[0]->GetClass()));
NObj->SetNumberField(TEXT("extraCount"), CountA - CountB);
OnlyInA.Add(MakeShared<FJsonValueObject>(NObj));
}
@@ -161,7 +161,7 @@ public:
{
TSharedRef<FJsonObject> NObj = MakeShared<FJsonObject>();
NObj->SetStringField(TEXT("title"), Pair.Key);
NObj->SetStringField(TEXT("class"), Pair.Value[0]->GetClass()->GetName());
NObj->SetStringField(TEXT("class"), MCPUtils::FormatName(Pair.Value[0]->GetClass()));
NObj->SetNumberField(TEXT("extraCount"), CountB - CountA);
OnlyInB.Add(MakeShared<FJsonValueObject>(NObj));
}
@@ -170,9 +170,9 @@ public:
// Connection diff: use connection key approach
auto MakeConnKey = [](UEdGraphPin* SrcPin, UEdGraphPin* TgtPin) -> FString
{
FString SrcTitle = SrcPin->GetOwningNode()->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
FString TgtTitle = TgtPin->GetOwningNode()->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
return FString::Printf(TEXT("%s|%s|%s|%s"), *SrcTitle, *SrcPin->PinName.ToString(), *TgtTitle, *TgtPin->PinName.ToString());
FString SrcTitle = MCPUtils::FormatName(SrcPin->GetOwningNode());
FString TgtTitle = MCPUtils::FormatName(TgtPin->GetOwningNode());
return FString::Printf(TEXT("%s|%s|%s|%s"), *SrcTitle, *MCPUtils::FormatName(SrcPin), *TgtTitle, *MCPUtils::FormatName(TgtPin));
};
TSet<FString> ConnectionsA, ConnectionsB;

View File

@@ -2,15 +2,13 @@
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "Engine/World.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "UMCPHandler_DisconnectBlueprintPins.generated.h"
#include "UMCPHandler_DisconnectPins.generated.h"
// ---------------------------------------------------------------------------
@@ -23,21 +21,15 @@ struct FDisconnectPinEntry
GENERATED_BODY()
UPROPERTY()
FString Node;
UPROPERTY()
FString PinName;
FString Pin;
UPROPERTY(meta=(Optional))
FString TargetNode;
UPROPERTY(meta=(Optional))
FString TargetPinName;
FString TargetPin;
};
UCLASS()
class UMCPHandler_DisconnectBlueprintPins : public UObject, public IMCPHandler
class UMCPHandler_DisconnectPins : public UObject, public IMCPHandler
{
GENERATED_BODY()
@@ -45,7 +37,7 @@ public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Array of {node, pinName, targetNode?, targetPinName?} objects. If target is omitted, all connections on the pin are broken."))
UPROPERTY(meta=(Description="Array of {pin, targetPin?} objects. Each pin is a path like node:MyNode,pin:Output. If targetPin is omitted, all connections on the pin are broken."))
FMCPJsonArray Disconnections;
virtual FString GetDescription() const override
@@ -57,9 +49,9 @@ public:
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();
MCPFetcher F(Result);
UBlueprint* BP = F.Walk(Blueprint).Cast<UBlueprint>();
if (!BP) return;
TArray<TSharedPtr<FJsonValue>> Results;
int32 SuccessCount = 0;
@@ -73,45 +65,28 @@ public:
FDisconnectPinEntry Entry;
if (!MCPUtils::PopulateFromJson(FDisconnectPinEntry::StaticStruct(), &Entry, DiscVal, &*EntryResult)) continue;
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.Node);
if (!Node)
{
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.Node));
continue;
}
UEdGraphPin* Pin = Node->FindPin(FName(*Entry.PinName));
if (!Pin)
{
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *Entry.PinName, *Entry.Node));
continue;
}
MCPFetcher FP(EntryResult, BP);
UEdGraphPin* Pin = FP.Walk(Entry.Pin).Cast<UEdGraphPin>();
if (!Pin) continue;
int32 DisconnectedCount = 0;
if (!Entry.TargetNode.IsEmpty() && !Entry.TargetPinName.IsEmpty())
if (!Entry.TargetPin.IsEmpty())
{
UEdGraphNode* TargetNode = MCPUtils::FindNodeByGuid(BP, Entry.TargetNode);
if (!TargetNode)
MCPFetcher FT(EntryResult, BP);
UEdGraphPin* Target = FT.Walk(Entry.TargetPin).Cast<UEdGraphPin>();
if (!Target) continue;
if (!Pin->LinkedTo.Contains(Target))
{
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target node '%s' not found"), *Entry.TargetNode));
EntryResult->SetStringField(TEXT("error"), FString::Printf(
TEXT("%s.%s is not connected to %s.%s"),
*MCPUtils::FormatName(Pin->GetOwningNode()), *MCPUtils::FormatName(Pin),
*MCPUtils::FormatName(Target->GetOwningNode()), *MCPUtils::FormatName(Target)));
continue;
}
UEdGraphPin* TargetPin = TargetNode->FindPin(FName(*Entry.TargetPinName));
if (!TargetPin)
{
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), *Entry.TargetPinName, *Entry.TargetNode));
continue;
}
if (!Pin->LinkedTo.Contains(TargetPin))
{
EntryResult->SetStringField(TEXT("error"), TEXT("The specified pins are not connected to each other"));
continue;
}
Pin->BreakLinkTo(TargetPin);
Pin->BreakLinkTo(Target);
DisconnectedCount = 1;
}
else

View File

@@ -2,24 +2,13 @@
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "Engine/World.h"
#include "Engine/Level.h"
#include "Engine/LevelScriptBlueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "K2Node_CallFunction.h"
#include "K2Node_Event.h"
#include "K2Node_CustomEvent.h"
#include "K2Node_VariableGet.h"
#include "K2Node_VariableSet.h"
#include "K2Node_BreakStruct.h"
#include "K2Node_MakeStruct.h"
#include "K2Node_FunctionEntry.h"
#include "K2Node_EditablePinBase.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "Animation/AnimBlueprint.h"
#include "Animation/Skeleton.h"
#include "Engine/SimpleConstructionScript.h"
#include "Engine/SCS_Node.h"
#include "UMCPHandler_DumpBlueprint.generated.h"
@@ -38,15 +27,77 @@ public:
virtual FString GetDescription() const override
{
return TEXT("Load and serialize a Blueprint, returning its full structure including graphs, variables, and components.");
return TEXT("Dump a Blueprint's structure: variables, interfaces, components, "
"and graph names. Does not include graph contents (use DumpGraphs for that).");
}
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
{
MCPAssets<UBlueprint> Assets;
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
MCPFetcher F(Result);
UBlueprint* BP = F.Walk(Blueprint).Cast<UBlueprint>();
if (!BP) return;
TSharedRef<FJsonObject> Tmp = MCPUtils::SerializeBlueprint(Assets.Object());
MCPUtils::CopyJsonFields(&*Tmp, Result);
Result->SetStringField(TEXT("name"), MCPUtils::FormatName(BP));
Result->SetStringField(TEXT("parentClass"), BP->ParentClass ? MCPUtils::FormatName(BP->ParentClass) : TEXT("None"));
Result->SetStringField(TEXT("blueprintType"),
StaticEnum<EBlueprintType>()->GetNameStringByValue((int64)BP->BlueprintType));
// Animation Blueprint
if (UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP))
{
Result->SetBoolField(TEXT("isAnimBlueprint"), true);
if (AnimBP->TargetSkeleton)
{
Result->SetStringField(TEXT("targetSkeleton"), AnimBP->TargetSkeleton->GetName());
Result->SetStringField(TEXT("targetSkeletonPath"), AnimBP->TargetSkeleton->GetPathName());
}
}
// Variables
TArray<TSharedPtr<FJsonValue>> Vars;
for (const FBPVariableDescription& V : BP->NewVariables)
{
TSharedRef<FJsonObject> VJ = MakeShared<FJsonObject>();
VJ->SetStringField(TEXT("name"), MCPUtils::FormatName(V));
VJ->SetStringField(TEXT("type"), V.VarType.PinCategory.ToString());
if (V.VarType.PinSubCategoryObject.IsValid())
VJ->SetStringField(TEXT("subtype"), V.VarType.PinSubCategoryObject->GetName());
VJ->SetBoolField(TEXT("isArray"), V.VarType.IsArray());
VJ->SetBoolField(TEXT("isSet"), V.VarType.IsSet());
VJ->SetBoolField(TEXT("isMap"), V.VarType.IsMap());
VJ->SetStringField(TEXT("category"), V.Category.ToString());
VJ->SetStringField(TEXT("defaultValue"), V.DefaultValue);
Vars.Add(MakeShared<FJsonValueObject>(VJ));
}
Result->SetArrayField(TEXT("variables"), Vars);
// Interfaces
TArray<TSharedPtr<FJsonValue>> Ifaces;
for (const FBPInterfaceDescription& I : BP->ImplementedInterfaces)
{
if (I.Interface)
Ifaces.Add(MakeShared<FJsonValueString>(MCPUtils::FormatName(I.Interface)));
}
Result->SetArrayField(TEXT("interfaces"), Ifaces);
// Components
if (USimpleConstructionScript* SCS = BP->SimpleConstructionScript)
{
TArray<TSharedPtr<FJsonValue>> Comps;
for (USCS_Node* Node : SCS->GetAllNodes())
{
if (!Node || !Node->ComponentTemplate) continue;
TSharedRef<FJsonObject> CJ = MakeShared<FJsonObject>();
CJ->SetStringField(TEXT("name"), MCPUtils::FormatName(Node->ComponentTemplate));
CJ->SetStringField(TEXT("class"), MCPUtils::FormatName(Node->ComponentClass));
if (Node->ParentComponentOrVariableName != NAME_None)
CJ->SetStringField(TEXT("parent"), Node->ParentComponentOrVariableName.ToString());
Comps.Add(MakeShared<FJsonValueObject>(CJ));
}
Result->SetArrayField(TEXT("components"), Comps);
}
// Graph names (without contents)
Result->SetArrayField(TEXT("graphs"), MCPUtils::AllGraphNamesJson(BP));
}
};

View File

@@ -1,80 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "Engine/World.h"
#include "Engine/Level.h"
#include "Engine/LevelScriptBlueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "K2Node_CallFunction.h"
#include "K2Node_Event.h"
#include "K2Node_CustomEvent.h"
#include "K2Node_VariableGet.h"
#include "K2Node_VariableSet.h"
#include "K2Node_BreakStruct.h"
#include "K2Node_MakeStruct.h"
#include "K2Node_FunctionEntry.h"
#include "K2Node_EditablePinBase.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "UMCPHandler_DumpBlueprintGraph.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UMCPHandler_DumpBlueprintGraph : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Graph name to dump"))
FString Graph;
virtual FString GetDescription() const override
{
return TEXT("Dump the detailed node/pin structure of a specific graph within a Blueprint.");
}
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
{
// URL-decode graph name to handle spaces encoded as '+' or '%20'
FString DecodedGraphName = MCPUtils::UrlDecode(Graph);
MCPAssets<UBlueprint> Assets;
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
UBlueprint* BP = Assets.Object();
TArray<UEdGraph*> AllGraphs = MCPUtils::AllGraphs(BP);
for (UEdGraph* GraphObj : AllGraphs)
{
if (GraphObj->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase))
{
TSharedPtr<FJsonObject> GraphJson = MCPUtils::SerializeGraph(GraphObj);
if (GraphJson.IsValid())
{
MCPUtils::CopyJsonFields(GraphJson.Get(), Result);
return;
}
}
}
// Not found — list available graphs
TArray<TSharedPtr<FJsonValue>> GraphNames;
for (UEdGraph* GraphObj : AllGraphs)
{
GraphNames.Add(MakeShared<FJsonValueString>(GraphObj->GetName()));
}
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName));
Result->SetArrayField(TEXT("availableGraphs"), GraphNames);
}
};

View File

@@ -0,0 +1,71 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "BlueprintExporter.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "UMCPHandler_DumpGraphs.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UMCPHandler_DumpGraphs : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Path to a blueprint or graph, e.g. /Game/Foo or /Game/Foo,graph:EventGraph"))
FString Path;
virtual FString GetDescription() const override
{
return TEXT("Dump blueprint graphs as readable text. "
"If given a blueprint, dumps all graphs. If given a specific graph, dumps only that one.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
MCPFetcher F(Result);
F.Walk(Path);
if (!F.Ok()) return;
if (UEdGraph* Graph = Cast<UEdGraph>(F.Obj))
{
EmitGraph(Graph, Result);
return;
}
if (UBlueprint* BP = Cast<UBlueprint>(F.Obj))
{
TArray<UEdGraph*> Graphs = MCPUtils::AllGraphs(BP);
for (UEdGraph* Graph : Graphs)
{
Result.Appendf(TEXT("\n======== %s ========\n"), *MCPUtils::FormatName(Graph));
EmitGraph(Graph, Result);
}
return;
}
Result.Appendf(TEXT("ERROR: Expected a blueprint or graph, got %s\n"),
*F.Obj->GetClass()->GetName());
}
private:
void EmitGraph(UEdGraph* Graph, FStringBuilderBase& Result)
{
FlxBlueprintExporter Exporter(Graph);
Result.Append(Exporter.GetOutput());
FString Details = Exporter.GetDetails();
if (!Details.IsEmpty())
{
Result.Append(TEXT("\n"));
Result.Append(Details);
}
}
};

View File

@@ -174,8 +174,8 @@ public:
Entry->SetStringField(TEXT("newNodeId"), NewNode->NodeGuid.ToString());
Entry->SetNumberField(TEXT("posX"), NewNode->NodePosX);
Entry->SetNumberField(TEXT("posY"), NewNode->NodePosY);
Entry->SetStringField(TEXT("nodeClass"), NewNode->GetClass()->GetName());
Entry->SetStringField(TEXT("nodeTitle"), NewNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString());
Entry->SetStringField(TEXT("nodeClass"), MCPUtils::FormatName(NewNode->GetClass()));
Entry->SetStringField(TEXT("nodeTitle"), MCPUtils::FormatName(NewNode));
DuplicatedNodes.Add(MakeShared<FJsonValueObject>(Entry));
}

View File

@@ -2,52 +2,9 @@
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPServer.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "Materials/Material.h"
#include "Materials/MaterialInstanceConstant.h"
#include "Materials/MaterialFunction.h"
#include "Engine/World.h"
#include "Engine/LevelScriptBlueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "EdGraphSchema_K2.h"
#include "K2Node.h"
#include "K2Node_CallFunction.h"
#include "K2Node_Event.h"
#include "K2Node_CustomEvent.h"
#include "K2Node_FunctionEntry.h"
#include "K2Node_EditablePinBase.h"
#include "K2Node_VariableGet.h"
#include "K2Node_VariableSet.h"
#include "K2Node_BreakStruct.h"
#include "K2Node_MakeStruct.h"
#include "K2Node_DynamicCast.h"
#include "K2Node_CallParentFunction.h"
#include "K2Node_IfThenElse.h"
#include "K2Node_ExecutionSequence.h"
#include "K2Node_MacroInstance.h"
#include "K2Node_SpawnActorFromClass.h"
#include "K2Node_Select.h"
#include "K2Node_Knot.h"
#include "EdGraphNode_Comment.h"
#include "GameFramework/Actor.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonWriter.h"
#include "Serialization/JsonSerializer.h"
#include "UObject/SavePackage.h"
#include "UObject/UObjectIterator.h"
#include "Misc/PackageName.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "AssetToolsModule.h"
#include "IAssetTools.h"
#include "BlueprintNodeSpawner.h"
#include "UMCPHandler_GetNodeComment.generated.h"
@@ -61,10 +18,7 @@ class UMCPHandler_GetNodeComment : public UObject, public IMCPHandler
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Node GUID"))
UPROPERTY(meta=(Description="Path to the node, e.g. /Game/Foo,node:MyNode"))
FString Node;
virtual FString GetDescription() const override
@@ -75,15 +29,9 @@ public:
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();
UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node);
if (!FoundNode)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *Node));
}
MCPFetcher F(Result);
UEdGraphNode* FoundNode = F.Walk(Node).Cast<UEdGraphNode>();
if (!FoundNode) return;
Result->SetStringField(TEXT("comment"), FoundNode->NodeComment);
Result->SetBoolField(TEXT("commentBubbleVisible"), FoundNode->bCommentBubbleVisible);

View File

@@ -38,7 +38,7 @@ public:
UEdGraphPin* P = F.Walk(Pin).Cast<UEdGraphPin>();
if (!P) return;
Result->SetStringField(TEXT("pinName"), P->PinName.ToString());
Result->SetStringField(TEXT("pinName"), MCPUtils::FormatName(P));
Result->SetStringField(TEXT("direction"), P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output"));
Result->SetStringField(TEXT("type"), P->PinType.PinCategory.ToString());
@@ -79,8 +79,8 @@ public:
if (!Linked || !Linked->GetOwningNode()) continue;
TSharedRef<FJsonObject> CJ = MakeShared<FJsonObject>();
CJ->SetStringField(TEXT("nodeId"), Linked->GetOwningNode()->NodeGuid.ToString());
CJ->SetStringField(TEXT("pinName"), Linked->PinName.ToString());
CJ->SetStringField(TEXT("nodeTitle"), Linked->GetOwningNode()->GetNodeTitle(ENodeTitleType::FullTitle).ToString());
CJ->SetStringField(TEXT("pinName"), MCPUtils::FormatName(Linked));
CJ->SetStringField(TEXT("nodeTitle"), MCPUtils::FormatName(Linked->GetOwningNode()));
Conns.Add(MakeShared<FJsonValueObject>(CJ));
}
Result->SetArrayField(TEXT("connectedTo"), Conns);

View File

@@ -61,7 +61,7 @@ public:
if (Node->ComponentClass)
{
CompObj->SetStringField(TEXT("componentClass"), Node->ComponentClass->GetName());
CompObj->SetStringField(TEXT("componentClass"), MCPUtils::FormatName(Node->ComponentClass));
}
else
{

View File

@@ -47,7 +47,7 @@ public:
}
TSharedRef<FJsonObject> IfaceObj = MakeShared<FJsonObject>();
IfaceObj->SetStringField(TEXT("name"), IfaceDesc.Interface->GetName());
IfaceObj->SetStringField(TEXT("name"), MCPUtils::FormatName(IfaceDesc.Interface));
IfaceObj->SetStringField(TEXT("classPath"), IfaceDesc.Interface->GetPathName());
TArray<TSharedPtr<FJsonValue>> FuncArr;
@@ -55,7 +55,7 @@ public:
{
if (Graph)
{
FuncArr.Add(MakeShared<FJsonValueString>(Graph->GetName()));
FuncArr.Add(MakeShared<FJsonValueString>(MCPUtils::FormatName(Graph)));
}
}
IfaceObj->SetArrayField(TEXT("functions"), FuncArr);

View File

@@ -80,7 +80,7 @@ public:
UClass* OwnerClass = Func->GetOwnerClass();
if (OwnerClass)
{
FuncObj->SetStringField(TEXT("definedIn"), OwnerClass->GetName());
FuncObj->SetStringField(TEXT("definedIn"), MCPUtils::FormatName(OwnerClass));
}
// Function flags
@@ -124,7 +124,7 @@ public:
FuncList.Add(MakeShared<FJsonValueObject>(FuncObj));
}
Result->SetStringField(TEXT("className"), FoundClass->GetName());
Result->SetStringField(TEXT("className"), MCPUtils::FormatName(FoundClass));
Result->SetNumberField(TEXT("count"), FuncList.Num());
Result->SetArrayField(TEXT("functions"), FuncList);
}

View File

@@ -78,7 +78,7 @@ public:
UClass* OwnerClass = Prop->GetOwnerClass();
if (OwnerClass)
{
PropObj->SetStringField(TEXT("definedIn"), OwnerClass->GetName());
PropObj->SetStringField(TEXT("definedIn"), MCPUtils::FormatName(OwnerClass));
}
// Property flags
@@ -99,7 +99,7 @@ public:
PropList.Add(MakeShared<FJsonValueObject>(PropObj));
}
Result->SetStringField(TEXT("className"), FoundClass->GetName());
Result->SetStringField(TEXT("className"), MCPUtils::FormatName(FoundClass));
Result->SetNumberField(TEXT("count"), PropList.Num());
Result->SetArrayField(TEXT("properties"), PropList);
}

View File

@@ -128,9 +128,9 @@ public:
{
if (Node->bHasCompilerMessage)
{
FString NodeTitle = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
FString NodeTitle = MCPUtils::FormatName(Node);
FString NodeMsg = FString::Printf(TEXT("[%s] %s: %s"),
*Node->GetGraph()->GetName(), *NodeTitle, *Node->ErrorMsg);
*MCPUtils::FormatName(Node->GetGraph()), *NodeTitle, *Node->ErrorMsg);
if (Node->ErrorType == EMessageSeverity::Error)
{
ErrorsArr.Add(MakeShared<FJsonValueString>(NodeMsg));

View File

@@ -80,7 +80,7 @@ public:
{
if (IfaceDesc.Interface)
{
IfaceList.Add(MakeShared<FJsonValueString>(IfaceDesc.Interface->GetName()));
IfaceList.Add(MakeShared<FJsonValueString>(MCPUtils::FormatName(IfaceDesc.Interface)));
}
}
@@ -94,7 +94,7 @@ public:
FTopLevelAssetPath InterfacePath = FoundInterface->GetClassPathName();
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removing interface '%s' from Blueprint '%s' (preserveFunctions: %s)"),
*FoundInterface->GetName(), *Blueprint, PreserveFunctions ? TEXT("true") : TEXT("false"));
*MCPUtils::FormatName(FoundInterface), *Blueprint, PreserveFunctions ? TEXT("true") : TEXT("false"));
FBlueprintEditorUtils::RemoveInterface(BP, InterfacePath, PreserveFunctions);
@@ -102,8 +102,8 @@ public:
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed interface '%s' from '%s'"),
*FoundInterface->GetName(), *Blueprint);
*MCPUtils::FormatName(FoundInterface), *Blueprint);
Result->SetStringField(TEXT("interfaceName"), FoundInterface->GetName());
Result->SetStringField(TEXT("interfaceName"), MCPUtils::FormatName(FoundInterface));
}
};

View File

@@ -51,7 +51,7 @@ public:
// Strategy 1: Look for a K2Node_FunctionEntry in a function graph matching the name
for (UK2Node_FunctionEntry* FuncEntry : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
{
if (FuncEntry->GetGraph()->GetName().Equals(FunctionName, ESearchCase::IgnoreCase))
if (MCPUtils::Identifies(FunctionName, FuncEntry->GetGraph()))
{
EntryNode = FuncEntry;
FoundNodeType = TEXT("FunctionEntry");
@@ -80,7 +80,7 @@ public:
for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
{
Available.Add(MakeShared<FJsonValueString>(
FString::Printf(TEXT("function:%s"), *FE->GetGraph()->GetName())));
FString::Printf(TEXT("function:%s"), *MCPUtils::FormatName(FE->GetGraph()))));
}
for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
{

View File

@@ -48,7 +48,7 @@ public:
// Check if it's an UbergraphPage — disallow rename
for (UEdGraph* CandidateGraph : BP->UbergraphPages)
{
if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase))
if (CandidateGraph && MCPUtils::Identifies(Graph, CandidateGraph))
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Cannot rename UbergraphPage '%s'. EventGraph and other Ubergraph pages cannot be renamed."),
@@ -62,7 +62,7 @@ public:
for (UEdGraph* CandidateGraph : BP->FunctionGraphs)
{
if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase))
if (CandidateGraph && MCPUtils::Identifies(Graph, CandidateGraph))
{
TargetGraph = CandidateGraph;
GraphType = TEXT("function");
@@ -73,7 +73,7 @@ public:
{
for (UEdGraph* CandidateGraph : BP->MacroGraphs)
{
if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase))
if (CandidateGraph && MCPUtils::Identifies(Graph, CandidateGraph))
{
TargetGraph = CandidateGraph;
GraphType = TEXT("macro");
@@ -108,7 +108,7 @@ public:
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("newName"), MCPUtils::FormatName(TargetGraph));
Result->SetStringField(TEXT("graphType"), GraphType);
Result->SetBoolField(TEXT("saved"), bSaved);
}

View File

@@ -43,7 +43,7 @@ public:
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
UBlueprint* BP = Assets.Object();
FString OldParentName = BP->ParentClass ? BP->ParentClass->GetName() : TEXT("None");
FString OldParentName = BP->ParentClass ? MCPUtils::FormatName(BP->ParentClass) : TEXT("None");
// Find the new parent class
// Try C++ class first (e.g. "WebUIHUD" finds /Script/ModuleName.WebUIHUD)
@@ -85,11 +85,11 @@ public:
// 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());
*Blueprint, *OldParentName, *MCPUtils::FormatName(NewParentClassObj));
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Reparenting '%s' from '%s' to '%s'"),
*Blueprint, *OldParentName, *NewParentClassObj->GetName());
*Blueprint, *OldParentName, *MCPUtils::FormatName(NewParentClassObj));
// Perform reparent
BP->PreEditChange(nullptr);
@@ -105,7 +105,7 @@ public:
// Save
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
FString NewParentActualName = NewParentClassObj->GetName();
FString NewParentActualName = MCPUtils::FormatName(NewParentClassObj);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Reparent complete, save %s"),
bSaved ? TEXT("succeeded") : TEXT("failed"));

View File

@@ -202,9 +202,9 @@ public:
AtRisk->SetStringField(TEXT("type"), TEXT("connectionAtRisk"));
AtRisk->SetStringField(TEXT("functionName"), FuncName.ToString());
AtRisk->SetStringField(TEXT("nodeId"), CallNode->NodeGuid.ToString());
AtRisk->SetStringField(TEXT("pinName"), Pin->PinName.ToString());
AtRisk->SetStringField(TEXT("pinName"), MCPUtils::FormatName(Pin));
AtRisk->SetStringField(TEXT("connectedToNode"), Linked->GetOwningNode()->NodeGuid.ToString());
AtRisk->SetStringField(TEXT("connectedToPin"), Linked->PinName.ToString());
AtRisk->SetStringField(TEXT("connectedToPin"), MCPUtils::FormatName(Linked));
BrokenConnections.Add(MakeShared<FJsonValueObject>(AtRisk));
}
}

View File

@@ -0,0 +1,76 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h"
#include "UMCPHandler_SearchAssets.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UMCPHandler_SearchAssets : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Optional, Description="Substring to match against asset package paths"))
FString Search;
UPROPERTY(meta=(Optional, Description="Asset class name to filter by, e.g. Blueprint, Material, StaticMesh"))
FString Type;
UPROPERTY(meta=(Optional, Description="Maximum number of results (default 50)"))
int32 Limit = 50;
virtual FString GetDescription() const override
{
return TEXT("Search for assets by name and/or type. At least one of Search or Type must be specified.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
if (Search.IsEmpty() && Type.IsEmpty())
{
Result.Append(TEXT("ERROR: At least one of Search or Type must be specified\n"));
return;
}
MCPAssets<UObject> Assets;
// If a type is specified, find the UClass and filter by it
if (!Type.IsEmpty())
{
UClass* TypeClass = MCPUtils::FindClassByName(Type);
if (!TypeClass)
{
Result.Appendf(TEXT("ERROR: Unknown asset type '%s'\n"), *Type);
return;
}
Assets.NoScans().Scan(TypeClass);
}
if (!Search.IsEmpty())
{
Assets.Substring(Search);
}
Assets.AllContent().Limit(Limit).Errors(Result).Info();
const TArray<FAssetData>& AllData = Assets.AllData();
for (const FAssetData& Data : AllData)
{
Result.Appendf(TEXT("%s %s\n"),
*Data.AssetClassPath.GetAssetName().ToString(),
*Data.PackageName.ToString());
}
if (AllData.Num() >= Limit)
{
Result.Appendf(TEXT("WARNING: You reached the limit of %d, to raise it, specify the Limit parameter.\n"), Limit);
}
}
};

View File

@@ -125,7 +125,7 @@ public:
R->SetStringField(TEXT("blueprintPath"), BPPath);
R->SetStringField(TEXT("usage"), TEXT("functionParameter"));
R->SetStringField(TEXT("location"), FString::Printf(TEXT("%s.%s"),
*Node->GetGraph()->GetName(), *PinInfo->PinName.ToString()));
*MCPUtils::FormatName(Node->GetGraph()), *PinInfo->PinName.ToString()));
R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
R->SetStringField(TEXT("currentType"), PinInfo->PinType.PinCategory.ToString());
if (!ParamSubtype.IsEmpty())
@@ -172,7 +172,7 @@ public:
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("location"), MCPUtils::FormatName(Node->GetGraph()));
R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
R->SetStringField(TEXT("structType"), BreakNode->StructType->GetName());
if (bIsLevel)
@@ -188,7 +188,7 @@ public:
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("location"), MCPUtils::FormatName(Node->GetGraph()));
R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
R->SetStringField(TEXT("structType"), MakeNode->StructType->GetName());
if (bIsLevel)
@@ -214,10 +214,10 @@ public:
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()));
*MCPUtils::FormatName(Node),
*MCPUtils::FormatName(Pin)));
R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
R->SetStringField(TEXT("graph"), Node->GetGraph()->GetName());
R->SetStringField(TEXT("graph"), MCPUtils::FormatName(Node->GetGraph()));
R->SetStringField(TEXT("pinType"), Pin->PinType.PinCategory.ToString());
if (!PinSubtype.IsEmpty())
R->SetStringField(TEXT("pinSubtype"), PinSubtype);

View File

@@ -92,7 +92,7 @@ public:
if (ClassList.Num() >= Limit) continue; // Count but don't add beyond limit
TSharedRef<FJsonObject> ClassObj = MakeShared<FJsonObject>();
ClassObj->SetStringField(TEXT("name"), ClassName);
ClassObj->SetStringField(TEXT("name"), MCPUtils::FormatName(Class));
ClassObj->SetStringField(TEXT("fullPath"), Class->GetPathName());
// Determine if it's a Blueprint-generated class
@@ -102,7 +102,7 @@ public:
// Parent class
if (Class->GetSuperClass())
{
ClassObj->SetStringField(TEXT("parentClass"), Class->GetSuperClass()->GetName());
ClassObj->SetStringField(TEXT("parentClass"), MCPUtils::FormatName(Class->GetSuperClass()));
}
// Module/package info

View File

@@ -92,9 +92,9 @@ public:
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
R->SetStringField(TEXT("blueprint"), AssetName);
R->SetStringField(TEXT("blueprintPath"), AssetPath);
R->SetStringField(TEXT("graph"), Node->GetGraph()->GetName());
R->SetStringField(TEXT("nodeTitle"), Title);
R->SetStringField(TEXT("nodeClass"), Node->GetClass()->GetName());
R->SetStringField(TEXT("graph"), MCPUtils::FormatName(Node->GetGraph()));
R->SetStringField(TEXT("nodeTitle"), MCPUtils::FormatName(Node));
R->SetStringField(TEXT("nodeClass"), MCPUtils::FormatName(Node->GetClass()));
if (!FuncName.IsEmpty()) R->SetStringField(TEXT("functionName"), FuncName);
if (!EventName.IsEmpty()) R->SetStringField(TEXT("eventName"), EventName);
if (!VarName.IsEmpty()) R->SetStringField(TEXT("variableName"), VarName);

View File

@@ -151,7 +151,7 @@ public:
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("'%s' is not a subclass of '%s' (required by property '%s')"),
*ResolvedClass->GetName(), *MetaClass->GetName(), *Property));
*MCPUtils::FormatName(ResolvedClass), *MCPUtils::FormatName(MetaClass), *Property));
}
ClassProp->SetPropertyValue_InContainer(CDO, ResolvedClass);
}
@@ -160,7 +160,7 @@ public:
FSoftObjectPtr SoftPtr(ResolvedClass);
SoftClassProp->SetPropertyValue_InContainer(CDO, SoftPtr);
}
ActualNewValue = ResolvedClass->GetName();
ActualNewValue = MCPUtils::FormatName(ResolvedClass);
bSuccess = true;
}
// Handle object properties (TObjectPtr, UObject*)

View File

@@ -2,52 +2,10 @@
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPServer.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "Materials/Material.h"
#include "Materials/MaterialInstanceConstant.h"
#include "Materials/MaterialFunction.h"
#include "Engine/World.h"
#include "Engine/LevelScriptBlueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "EdGraphSchema_K2.h"
#include "K2Node.h"
#include "K2Node_CallFunction.h"
#include "K2Node_Event.h"
#include "K2Node_CustomEvent.h"
#include "K2Node_FunctionEntry.h"
#include "K2Node_EditablePinBase.h"
#include "K2Node_VariableGet.h"
#include "K2Node_VariableSet.h"
#include "K2Node_BreakStruct.h"
#include "K2Node_MakeStruct.h"
#include "K2Node_DynamicCast.h"
#include "K2Node_CallParentFunction.h"
#include "K2Node_IfThenElse.h"
#include "K2Node_ExecutionSequence.h"
#include "K2Node_MacroInstance.h"
#include "K2Node_SpawnActorFromClass.h"
#include "K2Node_Select.h"
#include "K2Node_Knot.h"
#include "EdGraphNode_Comment.h"
#include "GameFramework/Actor.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonWriter.h"
#include "Serialization/JsonSerializer.h"
#include "UObject/SavePackage.h"
#include "UObject/UObjectIterator.h"
#include "Misc/PackageName.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "AssetToolsModule.h"
#include "IAssetTools.h"
#include "BlueprintNodeSpawner.h"
#include "UMCPHandler_SetNodeComment.generated.h"
@@ -61,10 +19,7 @@ class UMCPHandler_SetNodeComment : public UObject, public IMCPHandler
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Node GUID"))
UPROPERTY(meta=(Description="Path to the node, e.g. /Game/Foo,node:MyNode"))
FString Node;
UPROPERTY(meta=(Description="Comment text to set"))
@@ -78,15 +33,9 @@ public:
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();
UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node);
if (!FoundNode)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *Node));
}
MCPFetcher F(Result);
UEdGraphNode* FoundNode = F.Walk(Node).Cast<UEdGraphNode>();
if (!FoundNode) return;
FString OldComment = FoundNode->NodeComment;
FoundNode->NodeComment = Comment;
@@ -98,10 +47,11 @@ public:
FoundNode->bCommentBubblePinned = true;
}
UBlueprint* BP = FBlueprintEditorUtils::FindBlueprintForNodeChecked(FoundNode);
FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Set comment on node '%s' in '%s'"),
*Node, *Blueprint);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Set comment on node '%s'"),
*MCPUtils::FormatName(FoundNode));
Result->SetStringField(TEXT("oldComment"), OldComment);
}

View File

@@ -2,52 +2,11 @@
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPServer.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "Materials/Material.h"
#include "Materials/MaterialInstanceConstant.h"
#include "Materials/MaterialFunction.h"
#include "Engine/World.h"
#include "Engine/LevelScriptBlueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "EdGraphSchema_K2.h"
#include "K2Node.h"
#include "K2Node_CallFunction.h"
#include "K2Node_Event.h"
#include "K2Node_CustomEvent.h"
#include "K2Node_FunctionEntry.h"
#include "K2Node_EditablePinBase.h"
#include "K2Node_VariableGet.h"
#include "K2Node_VariableSet.h"
#include "K2Node_BreakStruct.h"
#include "K2Node_MakeStruct.h"
#include "K2Node_DynamicCast.h"
#include "K2Node_CallParentFunction.h"
#include "K2Node_IfThenElse.h"
#include "K2Node_ExecutionSequence.h"
#include "K2Node_MacroInstance.h"
#include "K2Node_SpawnActorFromClass.h"
#include "K2Node_Select.h"
#include "K2Node_Knot.h"
#include "EdGraphNode_Comment.h"
#include "GameFramework/Actor.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonWriter.h"
#include "Serialization/JsonSerializer.h"
#include "UObject/SavePackage.h"
#include "UObject/UObjectIterator.h"
#include "Misc/PackageName.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "AssetToolsModule.h"
#include "IAssetTools.h"
#include "BlueprintNodeSpawner.h"
#include "UMCPHandler_SetNodePositions.generated.h"
@@ -91,9 +50,9 @@ public:
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();
MCPFetcher F(Result);
UBlueprint* BP = F.Walk(Blueprint).Cast<UBlueprint>();
if (!BP) return;
TArray<TSharedPtr<FJsonValue>> Results;
int32 SuccessCount = 0;
@@ -106,12 +65,9 @@ public:
FMoveNodeEntry Entry;
if (!MCPUtils::PopulateFromJson(FMoveNodeEntry::StaticStruct(), &Entry, NodeVal, &*EntryResult)) continue;
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.Node);
if (!Node)
{
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.Node));
continue;
}
MCPFetcher FN(EntryResult, BP);
UEdGraphNode* Node = FN.Node(Entry.Node).Cast<UEdGraphNode>();
if (!Node) continue;
int32 OldX = Node->NodePosX;
int32 OldY = Node->NodePosY;

View File

@@ -2,10 +2,9 @@
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "Engine/World.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
@@ -66,25 +65,12 @@ public:
FSetPinDefaultEntry Entry;
if (!MCPUtils::PopulateFromJson(FSetPinDefaultEntry::StaticStruct(), &Entry, PinVal, &*EntryResult)) continue;
MCPAssets<UBlueprint> Assets;
if (!Assets.Scan<UBlueprint>().Scan<UWorld>().Exact(Entry.Blueprint).Errors(&*EntryResult).ENone().ETwo().Load())
continue;
UBlueprint* BP = Assets.Object();
MCPFetcher FE(EntryResult);
FE.Walk(Entry.Blueprint).Node(Entry.Node).Pin(Entry.PinName);
UEdGraphPin* Pin = FE.Cast<UEdGraphPin>();
if (!Pin) continue;
UEdGraph* Graph = nullptr;
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.Node, &Graph);
if (!Node)
{
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.Node));
continue;
}
UEdGraphPin* Pin = Node->FindPin(FName(*Entry.PinName));
if (!Pin)
{
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *Entry.PinName, *Entry.Node));
continue;
}
UEdGraphNode* Node = Pin->GetOwningNode();
if (Pin->Direction != EGPD_Input)
{
@@ -92,7 +78,7 @@ public:
continue;
}
const UEdGraphSchema* Schema = Graph->GetSchema();
const UEdGraphSchema* Schema = Node->GetGraph()->GetSchema();
if (Schema)
{
FString ValidationError = Schema->IsPinDefaultValid(Pin, Entry.Value, nullptr, FText::GetEmpty());
@@ -110,7 +96,7 @@ public:
EntryResult->SetStringField(TEXT("oldValue"), OldValue);
SuccessCount++;
ModifiedNodes.Add(Node);
ModifiedBlueprints.Add(BP);
ModifiedBlueprints.Add(FBlueprintEditorUtils::FindBlueprintForNodeChecked(Node));
}
for (UEdGraphNode* Node : ModifiedNodes)

View File

@@ -109,7 +109,7 @@ public:
TArray<TSharedPtr<FJsonValue>> GraphNames;
for (UEdGraph* G : MCPUtils::AllGraphs(BP))
{
GraphNames.Add(MakeShared<FJsonValueString>(G->GetName()));
GraphNames.Add(MakeShared<FJsonValueString>(MCPUtils::FormatName(G)));
}
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName));
Result->SetArrayField(TEXT("availableGraphs"), GraphNames);
@@ -165,7 +165,7 @@ public:
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Spawned node '%s' (class %s) via action '%s' in graph '%s' of '%s'"),
*NewNode->NodeGuid.ToString(),
*NewNode->GetClass()->GetName(),
*MCPUtils::FormatName(NewNode->GetClass()),
*Entry.ActionName,
*DecodedGraphName,
*Blueprint);
@@ -174,8 +174,8 @@ public:
TSharedPtr<FJsonObject> NodeState = MCPUtils::SerializeNode(NewNode);
EntryResult->SetStringField(TEXT("nodeId"), NewNode->NodeGuid.ToString());
EntryResult->SetStringField(TEXT("nodeClass"), NewNode->GetClass()->GetName());
EntryResult->SetStringField(TEXT("nodeTitle"), NewNode->GetNodeTitle(ENodeTitleType::ListView).ToString());
EntryResult->SetStringField(TEXT("nodeClass"), MCPUtils::FormatName(NewNode->GetClass()));
EntryResult->SetStringField(TEXT("nodeTitle"), MCPUtils::FormatName(NewNode));
if (NodeState.IsValid())
{
EntryResult->SetObjectField(TEXT("node"), NodeState);

View File

@@ -55,7 +55,7 @@ public:
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: test-save — loaded '%s', GeneratedClass=%s"),
*BP->GetName(),
BP->GeneratedClass ? *BP->GeneratedClass->GetName() : TEXT("null"));
BP->GeneratedClass ? *MCPUtils::FormatName(BP->GeneratedClass) : TEXT("null"));
// Attempt save with NO modifications
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);

View File

@@ -14,7 +14,7 @@
#include "Handlers/UMCPHandler_CheckPinConnectionCompatibility.h"
#include "Handlers/UMCPHandler_CompileBlueprint.h"
#include "Handlers/UMCPHandler_CompileMaterial.h"
#include "Handlers/UMCPHandler_ConnectBlueprintPins.h"
#include "Handlers/UMCPHandler_ConnectPins.h"
#include "Handlers/UMCPHandler_ConnectMaterialExpressionPins.h"
#include "Handlers/UMCPHandler_CreateAnimBlueprintAsset.h"
#include "Handlers/UMCPHandler_CreateBlendSpaceAsset.h"
@@ -31,10 +31,10 @@
#include "Handlers/UMCPHandler_DeleteNodeFromGraph.h"
#include "Handlers/UMCPHandler_DescribeMaterialInEnglish.h"
#include "Handlers/UMCPHandler_DiffTwoBlueprints.h"
#include "Handlers/UMCPHandler_DisconnectBlueprintPins.h"
#include "Handlers/UMCPHandler_DisconnectPins.h"
#include "Handlers/UMCPHandler_DisconnectMaterialExpressionPin.h"
#include "Handlers/UMCPHandler_DumpBlueprint.h"
#include "Handlers/UMCPHandler_DumpBlueprintGraph.h"
#include "Handlers/UMCPHandler_DumpGraphs.h"
#include "Handlers/UMCPHandler_DumpMaterial.h"
#include "Handlers/UMCPHandler_DumpMaterialExpressionGraph.h"
#include "Handlers/UMCPHandler_DumpMaterialFunction.h"
@@ -67,6 +67,7 @@
#include "Handlers/UMCPHandler_ReparentMaterialInstance.h"
#include "Handlers/UMCPHandler_ReplaceFunctionCallsInBlueprint.h"
#include "Handlers/UMCPHandler_RestoreAsset.h"
#include "Handlers/UMCPHandler_SearchAssets.h"
#include "Handlers/UMCPHandler_SearchSpawnableNodeTypes.h"
#include "Handlers/UMCPHandler_SearchTypeUsageInBlueprints.h"
#include "Handlers/UMCPHandler_SearchUnrealClasses.h"

View File

@@ -127,29 +127,29 @@ void MCPUtils::AppendNumericSuffix(FString &Name, int32 N)
Name += FString::Printf(TEXT("_%d"), N - 1);
}
FString MCPUtils::FormatName(UWorld *World)
FString MCPUtils::FormatName(const UWorld *World)
{
return World->GetPathName();
}
FString MCPUtils::FormatName(UBlueprint *BP)
FString MCPUtils::FormatName(const UBlueprint *BP)
{
return BP->GetPathName();
}
FString MCPUtils::FormatName(UActorComponent *C)
FString MCPUtils::FormatName(const UActorComponent *C)
{
return C->GetName();
}
FString MCPUtils::FormatName(UEdGraph *Graph)
FString MCPUtils::FormatName(const UEdGraph *Graph)
{
FString Name = Graph->GetName();
SanitizeNameInPlace(Name);
return Name;
}
FString MCPUtils::FormatName(UEdGraphNode* Node)
FString MCPUtils::FormatName(const UEdGraphNode* Node)
{
// Sanitized first line of the node title.
FString Title = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
@@ -161,7 +161,7 @@ FString MCPUtils::FormatName(UEdGraphNode* Node)
return Title;
}
FString MCPUtils::FormatName(UEdGraphPin *Pin)
FString MCPUtils::FormatName(const UEdGraphPin *Pin)
{
FString Name = Pin->PinName.ToString();
SanitizeNameInPlace(Name);
@@ -198,34 +198,34 @@ bool MCPUtils::Identifies(const FString &Name, const UClass *Class)
// Identifies
// ============================================================
bool MCPUtils::Identifies(const FString &Name, UWorld *World)
bool MCPUtils::Identifies(const FString &Name, const UWorld *World)
{
return FormatName(World).Equals(Name, ESearchCase::IgnoreCase);
}
bool MCPUtils::Identifies(const FString &Name, UBlueprint *BP)
bool MCPUtils::Identifies(const FString &Name, const UBlueprint *BP)
{
return FormatName(BP).Equals(Name, ESearchCase::IgnoreCase);
}
bool MCPUtils::Identifies(const FString &Name, UActorComponent *C)
bool MCPUtils::Identifies(const FString &Name, const UActorComponent *C)
{
return FormatName(C).Equals(Name, ESearchCase::IgnoreCase);
}
bool MCPUtils::Identifies(const FString &Name, UEdGraph *Graph)
bool MCPUtils::Identifies(const FString &Name, const UEdGraph *Graph)
{
return FormatName(Graph).Equals(Name, ESearchCase::IgnoreCase);
}
bool MCPUtils::Identifies(const FString &Name, UEdGraphNode* Node)
bool MCPUtils::Identifies(const FString &Name, const UEdGraphNode* Node)
{
if (Node->NodeGuid.ToString().Equals(Name, ESearchCase::IgnoreCase))
return true;
return FormatName(Node).Equals(Name, ESearchCase::IgnoreCase);
}
bool MCPUtils::Identifies(const FString &Name, UEdGraphPin *Pin)
bool MCPUtils::Identifies(const FString &Name, const UEdGraphPin *Pin)
{
return FormatName(Pin).Equals(Name, ESearchCase::IgnoreCase);
}

View File

@@ -81,6 +81,7 @@ struct MCPErrorCallback
MCPErrorCallback(std::nullptr_t);
MCPErrorCallback(FString& OutError);
MCPErrorCallback(FJsonObject* Result);
MCPErrorCallback(const TSharedRef<FJsonObject>& Result) : MCPErrorCallback(&*Result) {}
MCPErrorCallback(FStringBuilderBase& OutResult);
void SetError(const FString& Msg) const { Func(Msg); }
@@ -104,12 +105,12 @@ public:
//
////////////////////////////////////////////////////////
static FString FormatName(UWorld *World);
static FString FormatName(UBlueprint *BP);
static FString FormatName(UActorComponent *C);
static FString FormatName(UEdGraph *Graph);
static FString FormatName(UEdGraphNode* Node);
static FString FormatName(UEdGraphPin *Pin);
static FString FormatName(const UWorld *World);
static FString FormatName(const UBlueprint *BP);
static FString FormatName(const UActorComponent *C);
static FString FormatName(const UEdGraph *Graph);
static FString FormatName(const UEdGraphNode* Node);
static FString FormatName(const UEdGraphPin *Pin);
static FString FormatName(const FMemberReference &Ref);
static FString FormatName(const FBPVariableDescription &Var);
static FString FormatName(const UClass *Class);
@@ -126,12 +127,12 @@ public:
//
////////////////////////////////////////////////////////
static bool Identifies(const FString &Name, UWorld *World);
static bool Identifies(const FString &Name, UBlueprint *BP);
static bool Identifies(const FString &Name, UActorComponent *C);
static bool Identifies(const FString &Name, UEdGraph *Graph);
static bool Identifies(const FString &Name, UEdGraphNode* Node);
static bool Identifies(const FString &Name, UEdGraphPin *Pin);
static bool Identifies(const FString &Name, const UWorld *World);
static bool Identifies(const FString &Name, const UBlueprint *BP);
static bool Identifies(const FString &Name, const UActorComponent *C);
static bool Identifies(const FString &Name, const UEdGraph *Graph);
static bool Identifies(const FString &Name, const UEdGraphNode* Node);
static bool Identifies(const FString &Name, const UEdGraphPin *Pin);
static bool Identifies(const FString &Name, const FMemberReference &Ref);
static bool Identifies(const FString &Name, const UClass *Class);