Lots of work on MCP Handlers

This commit is contained in:
2026-03-06 02:50:02 -05:00
parent d85b62027c
commit 282548e2f3
9 changed files with 405 additions and 428 deletions

Binary file not shown.

View File

@@ -156,7 +156,7 @@ void UMCPHandler_AddInterface::Handle(const FJsonObject* Json, FJsonObject* Resu
} }
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
if (Save) Helper->SaveBlueprintPackage(BP);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added interface '%s' to '%s' (%d function stubs)"), UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added interface '%s' to '%s' (%d function stubs)"),
*InterfaceClass->GetName(), *Blueprint, AddedFunctions.Num()); *InterfaceClass->GetName(), *Blueprint, AddedFunctions.Num());
@@ -244,7 +244,7 @@ void UMCPHandler_RemoveInterface::Handle(const FJsonObject* Json, FJsonObject* R
FBlueprintEditorUtils::RemoveInterface(BP, InterfacePath, PreserveFunctions); FBlueprintEditorUtils::RemoveInterface(BP, InterfacePath, PreserveFunctions);
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
if (Save) Helper->SaveBlueprintPackage(BP);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed interface '%s' from '%s'"), UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed interface '%s' from '%s'"),
*FoundInterface->GetName(), *Blueprint); *FoundInterface->GetName(), *Blueprint);

View File

@@ -804,196 +804,104 @@ void FBlueprintMCPServer::HandleRefreshAllNodes(const FJsonObject* Json, FJsonOb
} }
// ============================================================ // ============================================================
// HandleSetPinDefault — set the default value of a pin on a node // SetPinDefault — set the default value of a pin on a node
// ============================================================ // ============================================================
void FBlueprintMCPServer::HandleSetPinDefault(const FJsonObject* Json, FJsonObject* Result) void UMCPHandler_SetPinDefault::Handle(const FJsonObject* Json, FJsonObject* Result)
{ {
// Check for batch mode MCPHelper* Helper = MCPHelper::Get();
const TArray<TSharedPtr<FJsonValue>>* BatchArray = nullptr;
if (Json->TryGetArrayField(TEXT("batch"), BatchArray) && BatchArray && BatchArray->Num() > 0) TArray<TSharedPtr<FJsonValue>> Results;
int32 SuccessCount = 0;
TSet<UEdGraphNode*> ModifiedNodes;
TSet<UBlueprint*> ModifiedBlueprints;
for (const TSharedPtr<FJsonValue>& PinVal : Pins.Array)
{ {
// Batch mode: process multiple pin default operations TSharedRef<FJsonObject> EntryResult = MakeShared<FJsonObject>();
TArray<TSharedPtr<FJsonValue>> Results; Results.Add(MakeShared<FJsonValueObject>(EntryResult));
int32 SuccessCount = 0;
TSet<UBlueprint*> ModifiedBlueprints;
for (const TSharedPtr<FJsonValue>& OpVal : *BatchArray) FSetPinDefaultEntry Entry;
FString PopulateError = Helper->PopulateFromJson(FSetPinDefaultEntry::StaticStruct(), &Entry, PinVal);
if (!PopulateError.IsEmpty())
{ {
TSharedPtr<FJsonObject> OpObj = OpVal->AsObject(); EntryResult->SetStringField(TEXT("error"), PopulateError);
if (!OpObj.IsValid()) continue;
{
TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>();
Entry->SetStringField(TEXT("error"), TEXT("Invalid batch entry"));
Results.Add(MakeShared<FJsonValueObject>(Entry));
continue;
}
FString OpBlueprint = OpObj->GetStringField(TEXT("blueprint"));
FString OpNodeId = OpObj->GetStringField(TEXT("nodeId"));
FString OpPinName = OpObj->GetStringField(TEXT("pinName"));
FString OpValue = OpObj->GetStringField(TEXT("value"));
TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>();
Entry->SetStringField(TEXT("blueprint"), OpBlueprint);
Entry->SetStringField(TEXT("nodeId"), OpNodeId);
Entry->SetStringField(TEXT("pinName"), OpPinName);
if (OpBlueprint.IsEmpty() || OpNodeId.IsEmpty() || OpPinName.IsEmpty())
{
Entry->SetStringField(TEXT("error"), TEXT("Missing required fields: blueprint, nodeId, pinName"));
Results.Add(MakeShared<FJsonValueObject>(Entry));
continue;
}
FString LoadError;
UBlueprint* BP = LoadBlueprintByName(OpBlueprint, LoadError);
if (!BP)
{
Entry->SetStringField(TEXT("error"), LoadError);
Results.Add(MakeShared<FJsonValueObject>(Entry));
continue;
}
UEdGraph* Graph = nullptr;
UEdGraphNode* Node = FindNodeByGuid(BP, OpNodeId, &Graph);
if (!Node)
{
Entry->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *OpNodeId));
Results.Add(MakeShared<FJsonValueObject>(Entry));
continue;
}
UEdGraphPin* Pin = Node->FindPin(FName(*OpPinName));
if (!Pin)
{
Entry->SetStringField(TEXT("error"), FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *OpPinName, *OpNodeId));
Results.Add(MakeShared<FJsonValueObject>(Entry));
continue;
}
if (Pin->Direction != EGPD_Input)
{
Entry->SetStringField(TEXT("error"), FString::Printf(TEXT("Pin '%s' is an output pin"), *OpPinName));
Results.Add(MakeShared<FJsonValueObject>(Entry));
continue;
}
FString OldValue = Pin->DefaultValue;
const UEdGraphSchema* Schema = Graph->GetSchema();
if (Schema)
{
Schema->TrySetDefaultValue(*Pin, OpValue);
}
else
{
Pin->DefaultValue = OpValue;
}
Entry->SetBoolField(TEXT("success"), true);
Entry->SetStringField(TEXT("oldValue"), OldValue);
Entry->SetStringField(TEXT("newValue"), Pin->DefaultValue);
Results.Add(MakeShared<FJsonValueObject>(Entry));
SuccessCount++;
ModifiedBlueprints.Add(BP);
} }
// Save all modified blueprints EntryResult->SetStringField(TEXT("blueprint"), Entry.Blueprint);
bool bAllSaved = true; EntryResult->SetStringField(TEXT("nodeId"), Entry.NodeId);
for (UBlueprint* BP : ModifiedBlueprints) EntryResult->SetStringField(TEXT("pinName"), Entry.PinName);
FString LoadError;
UBlueprint* BP = Helper->LoadBlueprintByName(Entry.Blueprint, LoadError);
if (!BP)
{ {
FBlueprintEditorUtils::MarkBlueprintAsModified(BP); EntryResult->SetStringField(TEXT("error"), LoadError);
FKismetEditorUtilities::CompileBlueprint(BP); continue;
if (!SaveBlueprintPackage(BP)) }
UEdGraph* Graph = nullptr;
UEdGraphNode* Node = Helper->FindNodeByGuid(BP, Entry.NodeId, &Graph);
if (!Node)
{
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.NodeId));
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.NodeId));
continue;
}
if (Pin->Direction != EGPD_Input)
{
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Pin '%s' is an output pin"), *Entry.PinName));
continue;
}
const UEdGraphSchema* Schema = Graph->GetSchema();
if (Schema)
{
FString ValidationError = Schema->IsPinDefaultValid(Pin, Entry.Value, nullptr, FText::GetEmpty());
if (!ValidationError.IsEmpty())
{ {
bAllSaved = false; EntryResult->SetStringField(TEXT("error"), FString::Printf(
TEXT("Invalid value for pin '%s': %s"), *Entry.PinName, *ValidationError));
continue;
} }
} }
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Batch SetPinDefault — %d/%d succeeded, save %s"), FString OldValue = Pin->DefaultValue;
SuccessCount, BatchArray->Num(), bAllSaved ? TEXT("true") : TEXT("false")); Pin->DefaultValue = Entry.Value;
Result->SetBoolField(TEXT("success"), true); EntryResult->SetBoolField(TEXT("success"), true);
Result->SetNumberField(TEXT("successCount"), SuccessCount); EntryResult->SetStringField(TEXT("oldValue"), OldValue);
Result->SetNumberField(TEXT("totalCount"), BatchArray->Num()); EntryResult->SetStringField(TEXT("newValue"), Pin->DefaultValue);
Result->SetArrayField(TEXT("results"), Results); SuccessCount++;
Result->SetBoolField(TEXT("saved"), bAllSaved); ModifiedNodes.Add(Node);
return; ModifiedBlueprints.Add(BP);
} }
// Single-pin mode (existing logic) for (UEdGraphNode* Node : ModifiedNodes)
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
FString NodeId = Json->GetStringField(TEXT("nodeId"));
FString PinName = Json->GetStringField(TEXT("pinName"));
FString Value = Json->GetStringField(TEXT("value"));
if (BlueprintName.IsEmpty() || NodeId.IsEmpty() || PinName.IsEmpty())
{ {
return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, nodeId, pinName")); Node->ReconstructNode();
} }
// Load Blueprint for (UBlueprint* BP : ModifiedBlueprints)
FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP)
{ {
return MakeErrorJson(Result, LoadError); FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
} }
// Find node UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SetPinDefault — %d/%d succeeded"),
UEdGraph* Graph = nullptr; SuccessCount, Pins.Array.Num());
UEdGraphNode* Node = FindNodeByGuid(BP, NodeId, &Graph);
if (!Node)
{
return MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId));
}
// Find pin
UEdGraphPin* Pin = Node->FindPin(FName(*PinName));
if (!Pin)
{
return MakeErrorJson(Result, FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *PinName, *NodeId));
}
// Only allow setting defaults on input pins
if (Pin->Direction != EGPD_Input)
{
return MakeErrorJson(Result, FString::Printf(TEXT("Pin '%s' is an output pin — can only set defaults on input pins"), *PinName));
}
// Store old value for reporting
FString OldValue = Pin->DefaultValue;
// Use the schema to set the default value (handles type validation)
const UEdGraphSchema* Schema = Graph->GetSchema();
if (Schema)
{
Schema->TrySetDefaultValue(*Pin, Value);
}
else
{
// Fallback: set directly
Pin->DefaultValue = Value;
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SetPinDefault on '%s' pin '%s': '%s' -> '%s'"),
*Node->GetNodeTitle(ENodeTitleType::ListView).ToString(), *PinName, *OldValue, *Value);
// Mark modified and compile
FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
FKismetEditorUtilities::CompileBlueprint(BP);
// Save
bool bSaved = SaveBlueprintPackage(BP);
Result->SetBoolField(TEXT("success"), true); Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("blueprint"), BlueprintName); Result->SetNumberField(TEXT("successCount"), SuccessCount);
Result->SetStringField(TEXT("nodeId"), NodeId); Result->SetNumberField(TEXT("totalCount"), Pins.Array.Num());
Result->SetStringField(TEXT("pinName"), PinName); Result->SetArrayField(TEXT("results"), Results);
Result->SetStringField(TEXT("oldValue"), OldValue);
Result->SetStringField(TEXT("newValue"), Pin->DefaultValue);
Result->SetBoolField(TEXT("saved"), bSaved);
} }
// ============================================================ // ============================================================
@@ -2109,191 +2017,119 @@ void FBlueprintMCPServer::HandleSetBlueprintDefault(const FJsonObject* Json, FJs
} }
// ============================================================ // ============================================================
// HandleMoveNode — reposition one or more nodes in a blueprint graph // MoveNode — reposition one or more nodes in a blueprint graph
// ============================================================ // ============================================================
void FBlueprintMCPServer::HandleMoveNode(const FJsonObject* Json, FJsonObject* Result) void UMCPHandler_MoveNode::Handle(const FJsonObject* Json, FJsonObject* Result)
{ {
FString BlueprintName = Json->GetStringField(TEXT("blueprint")); MCPHelper* Helper = MCPHelper::Get();
if (BlueprintName.IsEmpty())
{
return MakeErrorJson(Result, TEXT("Missing required field: blueprint"));
}
// Load Blueprint
FString LoadError; FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return Helper->MakeErrorJson(Result, LoadError);
} }
// Check for batch mode TArray<TSharedPtr<FJsonValue>> Results;
const TArray<TSharedPtr<FJsonValue>>* NodesArray = nullptr; int32 SuccessCount = 0;
bool bBatchMode = Json->TryGetArrayField(TEXT("nodes"), NodesArray) && NodesArray && NodesArray->Num() > 0;
if (bBatchMode) for (const TSharedPtr<FJsonValue>& NodeVal : Nodes.Array)
{ {
TArray<TSharedPtr<FJsonValue>> Results; TSharedRef<FJsonObject> EntryResult = MakeShared<FJsonObject>();
int32 SuccessCount = 0; Results.Add(MakeShared<FJsonValueObject>(EntryResult));
for (const TSharedPtr<FJsonValue>& NodeVal : *NodesArray) FMoveNodeEntry Entry;
FString PopulateError = Helper->PopulateFromJson(FMoveNodeEntry::StaticStruct(), &Entry, NodeVal);
if (!PopulateError.IsEmpty())
{ {
TSharedPtr<FJsonObject> NodeObj = NodeVal->AsObject(); EntryResult->SetStringField(TEXT("error"), PopulateError);
if (!NodeObj.IsValid()) continue; continue;
FString NodeId = NodeObj->GetStringField(TEXT("nodeId"));
int32 X = (int32)NodeObj->GetNumberField(TEXT("x"));
int32 Y = (int32)NodeObj->GetNumberField(TEXT("y"));
TSharedRef<FJsonObject> EntryResult = MakeShared<FJsonObject>();
EntryResult->SetStringField(TEXT("nodeId"), NodeId);
UEdGraphNode* Node = FindNodeByGuid(BP, NodeId);
if (!Node)
{
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *NodeId));
Results.Add(MakeShared<FJsonValueObject>(EntryResult));
continue;
}
int32 OldX = Node->NodePosX;
int32 OldY = Node->NodePosY;
Node->NodePosX = X;
Node->NodePosY = Y;
EntryResult->SetBoolField(TEXT("success"), true);
EntryResult->SetNumberField(TEXT("oldX"), OldX);
EntryResult->SetNumberField(TEXT("oldY"), OldY);
EntryResult->SetNumberField(TEXT("newX"), Node->NodePosX);
EntryResult->SetNumberField(TEXT("newY"), Node->NodePosY);
Results.Add(MakeShared<FJsonValueObject>(EntryResult));
SuccessCount++;
} }
FBlueprintEditorUtils::MarkBlueprintAsModified(BP); EntryResult->SetStringField(TEXT("nodeId"), Entry.NodeId);
bool bSaved = SaveBlueprintPackage(BP);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: MoveNode batch — %d/%d succeeded, save %s"), UEdGraphNode* Node = Helper->FindNodeByGuid(BP, Entry.NodeId);
SuccessCount, NodesArray->Num(), bSaved ? TEXT("true") : TEXT("false")); if (!Node)
{
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.NodeId));
continue;
}
Result->SetBoolField(TEXT("success"), true); int32 OldX = Node->NodePosX;
Result->SetStringField(TEXT("blueprint"), BlueprintName); int32 OldY = Node->NodePosY;
Result->SetNumberField(TEXT("movedCount"), SuccessCount); Node->NodePosX = Entry.X;
Result->SetNumberField(TEXT("totalRequested"), NodesArray->Num()); Node->NodePosY = Entry.Y;
Result->SetArrayField(TEXT("results"), Results); EntryResult->SetBoolField(TEXT("success"), true);
Result->SetBoolField(TEXT("saved"), bSaved); EntryResult->SetNumberField(TEXT("oldX"), OldX);
return; EntryResult->SetNumberField(TEXT("oldY"), OldY);
EntryResult->SetNumberField(TEXT("newX"), Node->NodePosX);
EntryResult->SetNumberField(TEXT("newY"), Node->NodePosY);
SuccessCount++;
} }
// Single node mode
FString NodeId = Json->GetStringField(TEXT("nodeId"));
if (NodeId.IsEmpty())
{
return MakeErrorJson(Result, TEXT("Missing required field: nodeId (or use 'nodes' array for batch mode)"));
}
if (!Json->HasField(TEXT("x")) || !Json->HasField(TEXT("y")))
{
return MakeErrorJson(Result, TEXT("Missing required fields: x, y"));
}
int32 X = (int32)Json->GetNumberField(TEXT("x"));
int32 Y = (int32)Json->GetNumberField(TEXT("y"));
UEdGraphNode* Node = FindNodeByGuid(BP, NodeId);
if (!Node)
{
return MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId));
}
int32 OldX = Node->NodePosX;
int32 OldY = Node->NodePosY;
Node->NodePosX = X;
Node->NodePosY = Y;
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: MoveNode '%s' from (%d,%d) to (%d,%d)"),
*NodeId, OldX, OldY, X, Y);
FBlueprintEditorUtils::MarkBlueprintAsModified(BP); FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
bool bSaved = SaveBlueprintPackage(BP);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: MoveNode — %d/%d succeeded"),
SuccessCount, Nodes.Array.Num());
Result->SetBoolField(TEXT("success"), true); Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("blueprint"), BlueprintName); Result->SetStringField(TEXT("blueprint"), Blueprint);
Result->SetStringField(TEXT("nodeId"), NodeId); Result->SetNumberField(TEXT("movedCount"), SuccessCount);
Result->SetNumberField(TEXT("oldX"), OldX); Result->SetNumberField(TEXT("totalRequested"), Nodes.Array.Num());
Result->SetNumberField(TEXT("oldY"), OldY); Result->SetArrayField(TEXT("results"), Results);
Result->SetNumberField(TEXT("newX"), Node->NodePosX);
Result->SetNumberField(TEXT("newY"), Node->NodePosY);
Result->SetBoolField(TEXT("saved"), bSaved);
} }
// ============================================================ // ============================================================
// HandleDuplicateNodes — duplicate one or more nodes in a graph // DuplicateNodes — duplicate one or more nodes in a graph
// ============================================================ // ============================================================
void FBlueprintMCPServer::HandleDuplicateNodes(const FJsonObject* Json, FJsonObject* Result) void UMCPHandler_DuplicateNodes::Handle(const FJsonObject* Json, FJsonObject* Result)
{ {
FString BlueprintName = Json->GetStringField(TEXT("blueprint")); MCPHelper* Helper = MCPHelper::Get();
FString GraphName = Json->GetStringField(TEXT("graph"));
if (BlueprintName.IsEmpty() || GraphName.IsEmpty())
{
return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph"));
}
// Get node IDs
const TArray<TSharedPtr<FJsonValue>>* NodeIdsArray = nullptr;
if (!Json->TryGetArrayField(TEXT("nodeIds"), NodeIdsArray) || !NodeIdsArray || NodeIdsArray->Num() == 0)
{
return MakeErrorJson(Result, TEXT("Missing required field: nodeIds (array of node GUIDs)"));
}
int32 OffsetX = 50;
int32 OffsetY = 50;
if (Json->HasField(TEXT("offsetX")))
OffsetX = (int32)Json->GetNumberField(TEXT("offsetX"));
if (Json->HasField(TEXT("offsetY")))
OffsetY = (int32)Json->GetNumberField(TEXT("offsetY"));
// Load Blueprint
FString LoadError; FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return Helper->MakeErrorJson(Result, LoadError);
} }
// Find the target graph // Find the target graph
FString DecodedGraphName = UrlDecode(GraphName); FString DecodedGraphName = MCPHelper::UrlDecode(Graph);
UEdGraph* TargetGraph = nullptr; UEdGraph* TargetGraph = nullptr;
TArray<UEdGraph*> AllGraphs; TArray<UEdGraph*> AllGraphs;
BP->GetAllGraphs(AllGraphs); BP->GetAllGraphs(AllGraphs);
for (UEdGraph* Graph : AllGraphs) for (UEdGraph* G : AllGraphs)
{ {
if (Graph && Graph->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase)) if (G && G->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase))
{ {
TargetGraph = Graph; TargetGraph = G;
break; break;
} }
} }
if (!TargetGraph) if (!TargetGraph)
{ {
return MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName)); return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName));
}
if (NodeIds.Array.Num() == 0)
{
return Helper->MakeErrorJson(Result, TEXT("nodeIds array is empty"));
} }
// Find all source nodes // Find all source nodes
TArray<UEdGraphNode*> SourceNodes; TArray<UEdGraphNode*> SourceNodes;
TArray<FString> NotFound; TArray<FString> NotFound;
for (const TSharedPtr<FJsonValue>& IdVal : *NodeIdsArray) for (const TSharedPtr<FJsonValue>& IdVal : NodeIds.Array)
{ {
FString NodeId = IdVal->AsString(); FString NodeId = IdVal->AsString();
UEdGraphNode* Node = FindNodeByGuid(BP, NodeId); UEdGraphNode* Node = Helper->FindNodeByGuid(BP, NodeId);
if (Node) if (Node)
{ {
// Verify it's in the target graph
if (Node->GetGraph() == TargetGraph) if (Node->GetGraph() == TargetGraph)
{ {
SourceNodes.Add(Node); SourceNodes.Add(Node);
@@ -2311,11 +2147,11 @@ void FBlueprintMCPServer::HandleDuplicateNodes(const FJsonObject* Json, FJsonObj
if (SourceNodes.Num() == 0) if (SourceNodes.Num() == 0)
{ {
return MakeErrorJson(Result, TEXT("No valid nodes found to duplicate")); return Helper->MakeErrorJson(Result, TEXT("No valid nodes found to duplicate"));
} }
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Duplicating %d node(s) in graph '%s' of '%s'"), UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Duplicating %d node(s) in graph '%s' of '%s'"),
SourceNodes.Num(), *DecodedGraphName, *BlueprintName); SourceNodes.Num(), *DecodedGraphName, *Blueprint);
// Duplicate each node // Duplicate each node
TArray<TSharedPtr<FJsonValue>> DuplicatedNodes; TArray<TSharedPtr<FJsonValue>> DuplicatedNodes;
@@ -2323,7 +2159,6 @@ void FBlueprintMCPServer::HandleDuplicateNodes(const FJsonObject* Json, FJsonObj
for (UEdGraphNode* SourceNode : SourceNodes) for (UEdGraphNode* SourceNode : SourceNodes)
{ {
// Duplicate the node using DuplicateObject
UEdGraphNode* NewNode = DuplicateObject<UEdGraphNode>(SourceNode, TargetGraph); UEdGraphNode* NewNode = DuplicateObject<UEdGraphNode>(SourceNode, TargetGraph);
if (!NewNode) if (!NewNode)
{ {
@@ -2334,15 +2169,12 @@ void FBlueprintMCPServer::HandleDuplicateNodes(const FJsonObject* Json, FJsonObj
continue; continue;
} }
// Assign new GUID
NewNode->CreateNewGuid(); NewNode->CreateNewGuid();
OldToNewGuidMap.Add(SourceNode->NodeGuid, NewNode->NodeGuid); OldToNewGuidMap.Add(SourceNode->NodeGuid, NewNode->NodeGuid);
// Offset position
NewNode->NodePosX += OffsetX; NewNode->NodePosX += OffsetX;
NewNode->NodePosY += OffsetY; NewNode->NodePosY += OffsetY;
// Break all connections on the duplicate (they point to old pin instances)
for (UEdGraphPin* Pin : NewNode->Pins) for (UEdGraphPin* Pin : NewNode->Pins)
{ {
if (Pin) if (Pin)
@@ -2351,7 +2183,6 @@ void FBlueprintMCPServer::HandleDuplicateNodes(const FJsonObject* Json, FJsonObj
} }
} }
// Add to graph
TargetGraph->AddNode(NewNode, false, false); TargetGraph->AddNode(NewNode, false, false);
TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>(); TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>();
@@ -2365,17 +2196,15 @@ void FBlueprintMCPServer::HandleDuplicateNodes(const FJsonObject* Json, FJsonObj
} }
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
bool bSaved = SaveBlueprintPackage(BP);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Duplicated %d node(s), save %s"), UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Duplicated %d node(s)"),
DuplicatedNodes.Num(), bSaved ? TEXT("true") : TEXT("false")); DuplicatedNodes.Num());
Result->SetBoolField(TEXT("success"), true); Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("blueprint"), BlueprintName); Result->SetStringField(TEXT("blueprint"), Blueprint);
Result->SetStringField(TEXT("graph"), DecodedGraphName); Result->SetStringField(TEXT("graph"), DecodedGraphName);
Result->SetNumberField(TEXT("duplicatedCount"), DuplicatedNodes.Num()); Result->SetNumberField(TEXT("duplicatedCount"), DuplicatedNodes.Num());
Result->SetArrayField(TEXT("nodes"), DuplicatedNodes); Result->SetArrayField(TEXT("nodes"), DuplicatedNodes);
Result->SetBoolField(TEXT("saved"), bSaved);
if (NotFound.Num() > 0) if (NotFound.Num() > 0)
{ {
@@ -2634,61 +2463,90 @@ void UMCPHandler_SpawnNode::Handle(const FJsonObject* Json, FJsonObject* Result)
return; return;
} }
// Find the spawner by exact full name TArray<TSharedPtr<FJsonValue>> Results;
TArray<UBlueprintNodeSpawner*> Matches = FNodeActionSearch::FindSpawner(ActionName); int32 SuccessCount = 0;
if (Matches.Num() == 0)
{
return Helper->MakeErrorJson(Result, FString::Printf(
TEXT("No action found matching '%s'. Use search_node_actions to find available actions."),
*ActionName));
}
if (Matches.Num() > 1)
{
return Helper->MakeErrorJson(Result, FString::Printf(
TEXT("Ambiguous: %d spawners match '%s'. Cannot determine which one to use."),
Matches.Num(), *ActionName));
}
UBlueprintNodeSpawner* Spawner = Matches[0];
// Invoke the spawner for (const TSharedPtr<FJsonValue>& NodeVal : Nodes.Array)
FVector2D Location(PosX, PosY);
IBlueprintNodeBinder::FBindingSet Bindings;
UEdGraphNode* NewNode = Spawner->Invoke(TargetGraph, Bindings, Location);
if (!NewNode)
{ {
return Helper->MakeErrorJson(Result, TEXT("Spawner Invoke() returned null — node creation failed.")); TSharedRef<FJsonObject> EntryResult = MakeShared<FJsonObject>();
} Results.Add(MakeShared<FJsonValueObject>(EntryResult));
// Ensure valid GUID FSpawnNodeEntry Entry;
if (!NewNode->NodeGuid.IsValid()) FString PopulateError = Helper->PopulateFromJson(FSpawnNodeEntry::StaticStruct(), &Entry, NodeVal);
{ if (!PopulateError.IsEmpty())
NewNode->CreateNewGuid(); {
EntryResult->SetStringField(TEXT("error"), PopulateError);
continue;
}
EntryResult->SetStringField(TEXT("actionName"), Entry.ActionName);
// Find the spawner by exact full name
TArray<UBlueprintNodeSpawner*> Matches = FNodeActionSearch::FindSpawner(Entry.ActionName);
if (Matches.Num() == 0)
{
EntryResult->SetStringField(TEXT("error"), FString::Printf(
TEXT("No action found matching '%s'. Use search_node_actions to find available actions."),
*Entry.ActionName));
continue;
}
if (Matches.Num() > 1)
{
EntryResult->SetStringField(TEXT("error"), FString::Printf(
TEXT("Ambiguous: %d spawners match '%s'. Cannot determine which one to use."),
Matches.Num(), *Entry.ActionName));
continue;
}
UBlueprintNodeSpawner* Spawner = Matches[0];
// Invoke the spawner
FVector2D Location(Entry.PosX, Entry.PosY);
IBlueprintNodeBinder::FBindingSet Bindings;
UEdGraphNode* NewNode = Spawner->Invoke(TargetGraph, Bindings, Location);
if (!NewNode)
{
EntryResult->SetStringField(TEXT("error"), TEXT("Spawner Invoke() returned null — node creation failed."));
continue;
}
// Ensure valid GUID
if (!NewNode->NodeGuid.IsValid())
{
NewNode->CreateNewGuid();
}
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(),
*Entry.ActionName,
*DecodedGraphName,
*Blueprint);
// Serialize result
TSharedPtr<FJsonObject> NodeState = Helper->SerializeNode(NewNode);
EntryResult->SetBoolField(TEXT("success"), true);
EntryResult->SetStringField(TEXT("nodeId"), NewNode->NodeGuid.ToString());
EntryResult->SetStringField(TEXT("nodeClass"), NewNode->GetClass()->GetName());
EntryResult->SetStringField(TEXT("nodeTitle"), NewNode->GetNodeTitle(ENodeTitleType::ListView).ToString());
if (NodeState.IsValid())
{
EntryResult->SetObjectField(TEXT("node"), NodeState);
}
SuccessCount++;
} }
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
if (Save) Helper->SaveBlueprintPackage(BP);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Spawned node '%s' (class %s) via action '%s' in graph '%s' of '%s'"), UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SpawnNode — %d/%d succeeded in graph '%s' of '%s'"),
*NewNode->NodeGuid.ToString(), SuccessCount, Nodes.Array.Num(), *DecodedGraphName, *Blueprint);
*NewNode->GetClass()->GetName(),
*ActionName,
*DecodedGraphName,
*Blueprint);
// Serialize result
TSharedPtr<FJsonObject> NodeState = Helper->SerializeNode(NewNode);
Result->SetBoolField(TEXT("success"), true); Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("blueprint"), Blueprint); Result->SetStringField(TEXT("blueprint"), Blueprint);
Result->SetStringField(TEXT("graph"), DecodedGraphName); Result->SetStringField(TEXT("graph"), DecodedGraphName);
Result->SetStringField(TEXT("actionName"), ActionName); Result->SetNumberField(TEXT("successCount"), SuccessCount);
Result->SetStringField(TEXT("nodeId"), NewNode->NodeGuid.ToString()); Result->SetNumberField(TEXT("totalCount"), Nodes.Array.Num());
Result->SetStringField(TEXT("nodeClass"), NewNode->GetClass()->GetName()); Result->SetArrayField(TEXT("results"), Results);
Result->SetStringField(TEXT("nodeTitle"), NewNode->GetNodeTitle(ENodeTitleType::ListView).ToString());
if (NodeState.IsValid())
{
Result->SetObjectField(TEXT("node"), NodeState);
}
} }

View File

@@ -1,5 +1,6 @@
#include "BlueprintMCPServer.h" #include "BlueprintMCPServer.h"
#include "MCPHandler.h" #include "MCPHandler.h"
#include "UObject/StrongObjectPtr.h"
#include "Materials/MaterialExpression.h" #include "Materials/MaterialExpression.h"
#include "AssetRegistry/AssetRegistryModule.h" #include "AssetRegistry/AssetRegistryModule.h"
#include "AssetRegistry/IAssetRegistry.h" #include "AssetRegistry/IAssetRegistry.h"
@@ -616,9 +617,9 @@ bool FBlueprintMCPServer::Start(int32 InPort, bool bEditorMode)
Router->BindRoute(FHttpPath(TEXT("/api/refresh-all-nodes")), EHttpServerRequestVerbs::VERB_POST, Router->BindRoute(FHttpPath(TEXT("/api/refresh-all-nodes")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("refreshAllNodes"))); QueuedHandler(TEXT("refreshAllNodes")));
Router->BindRoute(FHttpPath(TEXT("/api/set-pin-default")), EHttpServerRequestVerbs::VERB_POST, Router->BindRoute(FHttpPath(TEXT("/api/set-pin-default")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("setPinDefault"))); QueuedHandler(TEXT("set_pin_default")));
Router->BindRoute(FHttpPath(TEXT("/api/move-node")), EHttpServerRequestVerbs::VERB_POST, Router->BindRoute(FHttpPath(TEXT("/api/move-node")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("moveNode"))); QueuedHandler(TEXT("move_node")));
Router->BindRoute(FHttpPath(TEXT("/api/get-node-comment")), EHttpServerRequestVerbs::VERB_POST, Router->BindRoute(FHttpPath(TEXT("/api/get-node-comment")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("getNodeComment"))); QueuedHandler(TEXT("getNodeComment")));
Router->BindRoute(FHttpPath(TEXT("/api/set-node-comment")), EHttpServerRequestVerbs::VERB_POST, Router->BindRoute(FHttpPath(TEXT("/api/set-node-comment")), EHttpServerRequestVerbs::VERB_POST,
@@ -640,7 +641,7 @@ bool FBlueprintMCPServer::Start(int32 InPort, bool bEditorMode)
Router->BindRoute(FHttpPath(TEXT("/api/delete-node")), EHttpServerRequestVerbs::VERB_POST, Router->BindRoute(FHttpPath(TEXT("/api/delete-node")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("deleteNode"))); QueuedHandler(TEXT("deleteNode")));
Router->BindRoute(FHttpPath(TEXT("/api/duplicate-nodes")), EHttpServerRequestVerbs::VERB_POST, Router->BindRoute(FHttpPath(TEXT("/api/duplicate-nodes")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("duplicateNodes"))); QueuedHandler(TEXT("duplicate_nodes")));
Router->BindRoute(FHttpPath(TEXT("/api/search-by-type")), EHttpServerRequestVerbs::VERB_GET, Router->BindRoute(FHttpPath(TEXT("/api/search-by-type")), EHttpServerRequestVerbs::VERB_GET,
QueuedHandler(TEXT("searchByType"))); QueuedHandler(TEXT("searchByType")));
Router->BindRoute(FHttpPath(TEXT("/api/validate-blueprint")), EHttpServerRequestVerbs::VERB_POST, Router->BindRoute(FHttpPath(TEXT("/api/validate-blueprint")), EHttpServerRequestVerbs::VERB_POST,
@@ -926,11 +927,16 @@ bool FBlueprintMCPServer::ProcessOneRequest()
GEditor->BeginTransaction(FText::FromString(FString::Printf(TEXT("BlueprintMCP: %s"), *Req->Endpoint))); GEditor->BeginTransaction(FText::FromString(FString::Printf(TEXT("BlueprintMCP: %s"), *Req->Endpoint)));
} }
UMCPHandler* Handler = NewObject<UMCPHandler>(GetTransientPackage(), *HandlerClass); TStrongObjectPtr<UMCPHandler> Handler(NewObject<UMCPHandler>(GetTransientPackage(), *HandlerClass));
if (PopulateHandlerFromJson(Handler, Params.Get(), &*Result)) FString PopulateError = PopulateFromJson(Handler->GetClass(), Handler.Get(), Params.Get());
if (PopulateError.IsEmpty())
{ {
Handler->Handle(Params.Get(), &*Result); Handler->Handle(Params.Get(), &*Result);
} }
else
{
MakeErrorJson(&*Result, PopulateError);
}
if (bIsMutation && GEditor) if (bIsMutation && GEditor)
{ {
@@ -979,11 +985,11 @@ void FBlueprintMCPServer::RegisterHandlers()
TEXT("connectPins"), TEXT("connectPins"),
TEXT("disconnectPin"), TEXT("disconnectPin"),
TEXT("refreshAllNodes"), TEXT("refreshAllNodes"),
TEXT("setPinDefault"), TEXT("set_pin_default"),
TEXT("moveNode"), TEXT("move_node"),
TEXT("changeStructNodeType"), TEXT("changeStructNodeType"),
TEXT("deleteNode"), TEXT("deleteNode"),
TEXT("duplicateNodes"), TEXT("duplicate_nodes"),
TEXT("addNode"), TEXT("addNode"),
TEXT("spawn_node"), TEXT("spawn_node"),
TEXT("setNodeComment"), TEXT("setNodeComment"),
@@ -1052,8 +1058,8 @@ void FBlueprintMCPServer::RegisterHandlers()
H(TEXT("connectPins"), &FBlueprintMCPServer::HandleConnectPins); H(TEXT("connectPins"), &FBlueprintMCPServer::HandleConnectPins);
H(TEXT("disconnectPin"), &FBlueprintMCPServer::HandleDisconnectPin); H(TEXT("disconnectPin"), &FBlueprintMCPServer::HandleDisconnectPin);
H(TEXT("refreshAllNodes"), &FBlueprintMCPServer::HandleRefreshAllNodes); H(TEXT("refreshAllNodes"), &FBlueprintMCPServer::HandleRefreshAllNodes);
H(TEXT("setPinDefault"), &FBlueprintMCPServer::HandleSetPinDefault); // set_pin_default is now handled by UMCPHandler_SetPinDefault (new-style registry)
H(TEXT("moveNode"), &FBlueprintMCPServer::HandleMoveNode); // move_node is now handled by UMCPHandler_MoveNode (new-style registry)
H(TEXT("getNodeComment"), &FBlueprintMCPServer::HandleGetNodeComment); H(TEXT("getNodeComment"), &FBlueprintMCPServer::HandleGetNodeComment);
H(TEXT("setNodeComment"), &FBlueprintMCPServer::HandleSetNodeComment); H(TEXT("setNodeComment"), &FBlueprintMCPServer::HandleSetNodeComment);
H(TEXT("getPinInfo"), &FBlueprintMCPServer::HandleGetPinInfo); H(TEXT("getPinInfo"), &FBlueprintMCPServer::HandleGetPinInfo);
@@ -1063,7 +1069,7 @@ void FBlueprintMCPServer::RegisterHandlers()
H(TEXT("listProperties"), &FBlueprintMCPServer::HandleListProperties); H(TEXT("listProperties"), &FBlueprintMCPServer::HandleListProperties);
H(TEXT("changeStructNodeType"), &FBlueprintMCPServer::HandleChangeStructNodeType); H(TEXT("changeStructNodeType"), &FBlueprintMCPServer::HandleChangeStructNodeType);
H(TEXT("deleteNode"), &FBlueprintMCPServer::HandleDeleteNode); H(TEXT("deleteNode"), &FBlueprintMCPServer::HandleDeleteNode);
H(TEXT("duplicateNodes"), &FBlueprintMCPServer::HandleDuplicateNodes); // duplicate_nodes is now handled by UMCPHandler_DuplicateNodes (new-style registry)
H(TEXT("validateBlueprint"), &FBlueprintMCPServer::HandleValidateBlueprint); H(TEXT("validateBlueprint"), &FBlueprintMCPServer::HandleValidateBlueprint);
H(TEXT("validateAllBlueprints"), &FBlueprintMCPServer::HandleValidateAllBlueprints); H(TEXT("validateAllBlueprints"), &FBlueprintMCPServer::HandleValidateAllBlueprints);
H(TEXT("addNode"), &FBlueprintMCPServer::HandleAddNode); H(TEXT("addNode"), &FBlueprintMCPServer::HandleAddNode);

View File

@@ -10,12 +10,12 @@ namespace MCPPopulate
// Try to set a single FProperty on a handler from a JSON field. // Try to set a single FProperty on a handler from a JSON field.
// Returns an empty string on success, or an error message on failure. // Returns an empty string on success, or an error message on failure.
FString SetPropertyFromJson( FString SetPropertyFromJson(
UMCPHandler* Handler, void* Container,
FProperty* Prop, FProperty* Prop,
const FString& FieldName, const FString& FieldName,
const FJsonObject* Json) const FJsonObject* Json)
{ {
void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(Handler); void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(Container);
// FString // FString
if (FStrProperty* StrProp = CastField<FStrProperty>(Prop)) if (FStrProperty* StrProp = CastField<FStrProperty>(Prop))
@@ -118,17 +118,29 @@ FString SetPropertyFromJson(
return FString(); return FString();
} }
// FMCPSubtree — stash the JSON subtree into the struct // FMCPJsonObject — stash a JSON object into the struct
if (FStructProperty* StructProp = CastField<FStructProperty>(Prop)) if (FStructProperty* StructProp = CastField<FStructProperty>(Prop))
{ {
if (StructProp->Struct == FMCPSubtree::StaticStruct()) if (StructProp->Struct == FMCPJsonObject::StaticStruct())
{ {
if (!Json->HasTypedField<EJson::Object>(FieldName)) if (!Json->HasTypedField<EJson::Object>(FieldName))
{ {
return FString::Printf(TEXT("'%s' must be an object"), *FieldName); return FString::Printf(TEXT("'%s' must be an object"), *FieldName);
} }
FMCPSubtree* Subtree = StructProp->ContainerPtrToValuePtr<FMCPSubtree>(Handler); FMCPJsonObject* Obj = StructProp->ContainerPtrToValuePtr<FMCPJsonObject>(Container);
Subtree->Json = Json->GetObjectField(FieldName); Obj->Json = Json->GetObjectField(FieldName);
return FString();
}
// FMCPJsonArray — stash a JSON array into the struct
if (StructProp->Struct == FMCPJsonArray::StaticStruct())
{
if (!Json->HasTypedField<EJson::Array>(FieldName))
{
return FString::Printf(TEXT("'%s' must be an array"), *FieldName);
}
FMCPJsonArray* Arr = StructProp->ContainerPtrToValuePtr<FMCPJsonArray>(Container);
Arr->Array = Json->GetArrayField(FieldName);
return FString(); return FString();
} }
} }
@@ -152,18 +164,28 @@ FString PropertyNameToJsonKey(const FString& PropName)
} // namespace MCPPopulate } // namespace MCPPopulate
bool FBlueprintMCPServer::PopulateHandlerFromJson( FString FBlueprintMCPServer::PopulateFromJson(
UMCPHandler* Handler, UStruct* StructType,
const FJsonObject* Json, void* Container,
FJsonObject* Result) const TSharedPtr<FJsonValue>& JsonValue)
{ {
UClass* HandlerClass = Handler->GetClass(); if (!JsonValue.IsValid() || (JsonValue->Type != EJson::Object))
{
return TEXT("Expected a JSON object");
}
return PopulateFromJson(StructType, Container, JsonValue->AsObject().Get());
}
FString FBlueprintMCPServer::PopulateFromJson(
UStruct* StructType,
void* Container,
const FJsonObject* Json)
{
// Build a set of known property names (as JSON keys) for the unknown-field check. // Build a set of known property names (as JSON keys) for the unknown-field check.
TSet<FString> KnownKeys; TSet<FString> KnownKeys;
TArray<FProperty*> Properties; TArray<FProperty*> Properties;
for (TFieldIterator<FProperty> It(HandlerClass, EFieldIterationFlags::None); It; ++It) for (TFieldIterator<FProperty> It(StructType, EFieldIterationFlags::None); It; ++It)
{ {
FProperty* Prop = *It; FProperty* Prop = *It;
Properties.Add(Prop); Properties.Add(Prop);
@@ -175,18 +197,7 @@ bool FBlueprintMCPServer::PopulateHandlerFromJson(
{ {
if (!KnownKeys.Contains(KV.Key)) if (!KnownKeys.Contains(KV.Key))
{ {
MakeErrorJson(Result, FString::Printf( return FString::Printf(TEXT("Unknown parameter '%s'"), *KV.Key);
TEXT("Unknown parameter '%s'"), *KV.Key));
Result->SetArrayField(TEXT("validParameters"),
[&]() {
TArray<TSharedPtr<FJsonValue>> Arr;
for (const FString& Key : KnownKeys)
{
Arr.Add(MakeShared<FJsonValueString>(Key));
}
return Arr;
}());
return false;
} }
} }
@@ -200,20 +211,17 @@ bool FBlueprintMCPServer::PopulateHandlerFromJson(
{ {
if (!bOptional) if (!bOptional)
{ {
MakeErrorJson(Result, FString::Printf( return FString::Printf(TEXT("Missing required parameter '%s'"), *JsonKey);
TEXT("Missing required parameter '%s'"), *JsonKey));
return false;
} }
continue; continue;
} }
FString Error = MCPPopulate::SetPropertyFromJson(Handler, Prop, JsonKey, Json); FString Error = MCPPopulate::SetPropertyFromJson(Container, Prop, JsonKey, Json);
if (!Error.IsEmpty()) if (!Error.IsEmpty())
{ {
MakeErrorJson(Result, Error); return Error;
return false;
} }
} }
return true; return FString();
} }

View File

@@ -34,9 +34,6 @@ public:
UPROPERTY(meta=(Description="Interface name (e.g. 'BPI_MyInterface') or native UInterface class name")) UPROPERTY(meta=(Description="Interface name (e.g. 'BPI_MyInterface') or native UInterface class name"))
FString InterfaceName; FString InterfaceName;
UPROPERTY(meta=(Optional, Description="Save the blueprint after adding the interface"))
bool Save = false;
virtual FString GetDescription() const override virtual FString GetDescription() const override
{ {
return TEXT("Add a Blueprint Interface implementation to a Blueprint. " return TEXT("Add a Blueprint Interface implementation to a Blueprint. "
@@ -61,9 +58,6 @@ public:
UPROPERTY(meta=(Optional, Description="If true, keep the function graphs as regular functions")) UPROPERTY(meta=(Optional, Description="If true, keep the function graphs as regular functions"))
bool PreserveFunctions = false; bool PreserveFunctions = false;
UPROPERTY(meta=(Optional, Description="Save the blueprint after removing the interface"))
bool Save = false;
virtual FString GetDescription() const override virtual FString GetDescription() const override
{ {
return TEXT("Remove a Blueprint Interface implementation from a Blueprint. " return TEXT("Remove a Blueprint Interface implementation from a Blueprint. "

View File

@@ -4,6 +4,122 @@
#include "MCPHandler.h" #include "MCPHandler.h"
#include "BlueprintMCPHandlers_Mutation.generated.h" #include "BlueprintMCPHandlers_Mutation.generated.h"
USTRUCT()
struct FSetPinDefaultEntry
{
GENERATED_BODY()
UPROPERTY()
FString Blueprint;
UPROPERTY()
FString NodeId;
UPROPERTY()
FString PinName;
UPROPERTY()
FString Value;
};
UCLASS(meta=(ToolName="set_pin_default"))
class UMCPHandler_SetPinDefault : public UMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Array of {blueprint, nodeId, pinName, value} objects"))
FMCPJsonArray Pins;
virtual FString GetDescription() const override
{
return TEXT("Set the default value of input pins on nodes.");
}
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override;
};
USTRUCT()
struct FMoveNodeEntry
{
GENERATED_BODY()
UPROPERTY()
FString NodeId;
UPROPERTY()
int32 X = 0;
UPROPERTY()
int32 Y = 0;
};
UCLASS(meta=(ToolName="move_node"))
class UMCPHandler_MoveNode : public UMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Array of {nodeId, x, y} objects"))
FMCPJsonArray Nodes;
virtual FString GetDescription() const override
{
return TEXT("Reposition one or more nodes in a Blueprint graph.");
}
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override;
};
UCLASS(meta=(ToolName="duplicate_nodes"))
class UMCPHandler_DuplicateNodes : public UMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Graph name"))
FString Graph;
UPROPERTY(meta=(Description="Array of node GUIDs to duplicate"))
FMCPJsonArray NodeIds;
UPROPERTY(meta=(Optional, Description="X offset for duplicated nodes"))
int32 OffsetX = 50;
UPROPERTY(meta=(Optional, Description="Y offset for duplicated nodes"))
int32 OffsetY = 50;
virtual FString GetDescription() const override
{
return TEXT("Duplicate one or more nodes in a Blueprint graph. "
"Creates copies offset from the originals with new GUIDs. "
"Connections are not preserved on the duplicates.");
}
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override;
};
USTRUCT()
struct FSpawnNodeEntry
{
GENERATED_BODY()
UPROPERTY()
FString ActionName;
UPROPERTY()
int32 PosX = 0;
UPROPERTY()
int32 PosY = 0;
};
UCLASS(meta=(ToolName="spawn_node")) UCLASS(meta=(ToolName="spawn_node"))
class UMCPHandler_SpawnNode : public UMCPHandler class UMCPHandler_SpawnNode : public UMCPHandler
{ {
@@ -16,23 +132,13 @@ public:
UPROPERTY(meta=(Description="Graph name (e.g. 'EventGraph')")) UPROPERTY(meta=(Description="Graph name (e.g. 'EventGraph')"))
FString Graph; FString Graph;
UPROPERTY(meta=(Description="Full action name from search_node_actions (e.g. 'Luprex|Lua|Read Lua Values')")) UPROPERTY(meta=(Description="Array of {actionName, posX, posY} objects. Use search_node_actions to find action names."))
FString ActionName; FMCPJsonArray Nodes;
UPROPERTY(meta=(Optional, Description="X position in the graph"))
int32 PosX = 0;
UPROPERTY(meta=(Optional, Description="Y position in the graph"))
int32 PosY = 0;
UPROPERTY(meta=(Optional, Description="Save the blueprint after spawning the node"))
bool Save = false;
virtual FString GetDescription() const override virtual FString GetDescription() const override
{ {
return TEXT("Create a node in a Blueprint graph using the editor's action database. " return TEXT("Create nodes in a Blueprint graph using the editor's action database. "
"Unlike add_node which only supports a fixed set of node types, spawn_node can create " "Can create ANY node type that appears in the editor's right-click menu, including custom K2 nodes. "
"ANY node type that appears in the editor's right-click menu, including custom K2 nodes. "
"Use search_node_actions first to find the exact action name."); "Use search_node_actions first to find the exact action name.");
} }

View File

@@ -142,7 +142,6 @@ private:
void HandleRemoveFunctionParameter(const FJsonObject* Json, FJsonObject* Result); void HandleRemoveFunctionParameter(const FJsonObject* Json, FJsonObject* Result);
void HandleDeleteAsset(const FJsonObject* Json, FJsonObject* Result); void HandleDeleteAsset(const FJsonObject* Json, FJsonObject* Result);
void HandleDeleteNode(const FJsonObject* Json, FJsonObject* Result); void HandleDeleteNode(const FJsonObject* Json, FJsonObject* Result);
void HandleDuplicateNodes(const FJsonObject* Json, FJsonObject* Result);
void HandleAddNode(const FJsonObject* Json, FJsonObject* Result); void HandleAddNode(const FJsonObject* Json, FJsonObject* Result);
void HandleRenameAsset(const FJsonObject* Json, FJsonObject* Result); void HandleRenameAsset(const FJsonObject* Json, FJsonObject* Result);
@@ -154,8 +153,6 @@ private:
void HandleConnectPins(const FJsonObject* Json, FJsonObject* Result); void HandleConnectPins(const FJsonObject* Json, FJsonObject* Result);
void HandleDisconnectPin(const FJsonObject* Json, FJsonObject* Result); void HandleDisconnectPin(const FJsonObject* Json, FJsonObject* Result);
void HandleRefreshAllNodes(const FJsonObject* Json, FJsonObject* Result); void HandleRefreshAllNodes(const FJsonObject* Json, FJsonObject* Result);
void HandleSetPinDefault(const FJsonObject* Json, FJsonObject* Result);
void HandleMoveNode(const FJsonObject* Json, FJsonObject* Result);
void HandleGetNodeComment(const FJsonObject* Json, FJsonObject* Result); void HandleGetNodeComment(const FJsonObject* Json, FJsonObject* Result);
void HandleSetNodeComment(const FJsonObject* Json, FJsonObject* Result); void HandleSetNodeComment(const FJsonObject* Json, FJsonObject* Result);
@@ -297,9 +294,10 @@ public:
static void CopyJsonFields(const FJsonObject* Source, FJsonObject* Dest); static void CopyJsonFields(const FJsonObject* Source, FJsonObject* Dest);
static FString UrlDecode(const FString& EncodedString); static FString UrlDecode(const FString& EncodedString);
// Populate a handler's UPROPERTY fields from JSON. // Populate UPROPERTY fields of a UStruct (or UClass) instance from JSON.
// Returns true on success, or sets error on Result and returns false. // Returns empty string on success, or an error message on failure.
bool PopulateHandlerFromJson(UMCPHandler* Handler, const FJsonObject* Json, FJsonObject* Result); FString PopulateFromJson(UStruct* StructType, void* Container, const TSharedPtr<FJsonValue>& JsonValue);
FString PopulateFromJson(UStruct* StructType, void* Container, const FJsonObject* Json);
// ----- Material helpers ----- // ----- Material helpers -----
/** Ensure that Material->MaterialGraph exists (creates it on demand for commandlet mode). */ /** Ensure that Material->MaterialGraph exists (creates it on demand for commandlet mode). */

View File

@@ -5,17 +5,24 @@
#include "Dom/JsonObject.h" #include "Dom/JsonObject.h"
#include "MCPHandler.generated.h" #include "MCPHandler.generated.h"
// Marker struct for handler parameters that accept a JSON subtree. // Marker struct for handler parameters that accept a JSON object.
// The parameter name is included in the tool schema as "type": "object". // PopulateFromJson stashes the actual JSON object into the Json field.
// PopulateHandlerFromJson stashes the actual JSON object into the Json field,
// so the handler can read it directly without parsing from the raw request.
USTRUCT() USTRUCT()
struct FMCPSubtree struct FMCPJsonObject
{ {
GENERATED_BODY() GENERATED_BODY()
TSharedPtr<FJsonObject> Json; TSharedPtr<FJsonObject> Json;
}; };
// Marker struct for handler parameters that accept a JSON array.
// PopulateFromJson stashes the actual JSON array into the Array field.
USTRUCT()
struct FMCPJsonArray
{
GENERATED_BODY()
TArray<TSharedPtr<FJsonValue>> Array;
};
// Base class for self-registering MCP tool handlers. // Base class for self-registering MCP tool handlers.
// //
// Subclasses declare their parameters as UPROPERTY fields, which are // Subclasses declare their parameters as UPROPERTY fields, which are