From 282548e2f36b4075ebcb07efe184b6bf953e52f8 Mon Sep 17 00:00:00 2001 From: jyelon Date: Fri, 6 Mar 2026 02:50:02 -0500 Subject: [PATCH] Lots of work on MCP Handlers --- Content/Widgets/WB_Hotkeys.uasset | 4 +- .../BlueprintMCPHandlers_Interfaces.cpp | 4 +- .../Private/BlueprintMCPHandlers_Mutation.cpp | 560 +++++++----------- .../Private/BlueprintMCPServer.cpp | 28 +- .../Private/MCPHandlerPopulate.cpp | 70 ++- .../Public/BlueprintMCPHandlers_Interfaces.h | 6 - .../Public/BlueprintMCPHandlers_Mutation.h | 134 ++++- .../BlueprintMCP/Public/BlueprintMCPServer.h | 10 +- .../Source/BlueprintMCP/Public/MCPHandler.h | 17 +- 9 files changed, 405 insertions(+), 428 deletions(-) diff --git a/Content/Widgets/WB_Hotkeys.uasset b/Content/Widgets/WB_Hotkeys.uasset index 9c43a773..ef5f8336 100644 --- a/Content/Widgets/WB_Hotkeys.uasset +++ b/Content/Widgets/WB_Hotkeys.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2cf72e771d99ac7b6c6c7f32cef5613f617ed47e9eeed41e4d10dedc54853f47 -size 282588 +oid sha256:43c0640d859e140bd0efeabcc932eb680f02203657da891981a3705acd9da499 +size 261317 diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Interfaces.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Interfaces.cpp index e8b27616..1b365fde 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Interfaces.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Interfaces.cpp @@ -156,7 +156,7 @@ void UMCPHandler_AddInterface::Handle(const FJsonObject* Json, FJsonObject* Resu } FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - if (Save) Helper->SaveBlueprintPackage(BP); + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added interface '%s' to '%s' (%d function stubs)"), *InterfaceClass->GetName(), *Blueprint, AddedFunctions.Num()); @@ -244,7 +244,7 @@ void UMCPHandler_RemoveInterface::Handle(const FJsonObject* Json, FJsonObject* R FBlueprintEditorUtils::RemoveInterface(BP, InterfacePath, PreserveFunctions); FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - if (Save) Helper->SaveBlueprintPackage(BP); + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed interface '%s' from '%s'"), *FoundInterface->GetName(), *Blueprint); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Mutation.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Mutation.cpp index 5e2e40ee..43ef061f 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Mutation.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Mutation.cpp @@ -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 - const TArray>* BatchArray = nullptr; - if (Json->TryGetArrayField(TEXT("batch"), BatchArray) && BatchArray && BatchArray->Num() > 0) + MCPHelper* Helper = MCPHelper::Get(); + + TArray> Results; + int32 SuccessCount = 0; + TSet ModifiedNodes; + TSet ModifiedBlueprints; + + for (const TSharedPtr& PinVal : Pins.Array) { - // Batch mode: process multiple pin default operations - TArray> Results; - int32 SuccessCount = 0; - TSet ModifiedBlueprints; + TSharedRef EntryResult = MakeShared(); + Results.Add(MakeShared(EntryResult)); - for (const TSharedPtr& OpVal : *BatchArray) + FSetPinDefaultEntry Entry; + FString PopulateError = Helper->PopulateFromJson(FSetPinDefaultEntry::StaticStruct(), &Entry, PinVal); + if (!PopulateError.IsEmpty()) { - TSharedPtr OpObj = OpVal->AsObject(); - if (!OpObj.IsValid()) - { - TSharedRef Entry = MakeShared(); - Entry->SetStringField(TEXT("error"), TEXT("Invalid batch entry")); - Results.Add(MakeShared(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 Entry = MakeShared(); - 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(Entry)); - continue; - } - - FString LoadError; - UBlueprint* BP = LoadBlueprintByName(OpBlueprint, LoadError); - if (!BP) - { - Entry->SetStringField(TEXT("error"), LoadError); - Results.Add(MakeShared(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(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(Entry)); - continue; - } - - if (Pin->Direction != EGPD_Input) - { - Entry->SetStringField(TEXT("error"), FString::Printf(TEXT("Pin '%s' is an output pin"), *OpPinName)); - Results.Add(MakeShared(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(Entry)); - SuccessCount++; - ModifiedBlueprints.Add(BP); + EntryResult->SetStringField(TEXT("error"), PopulateError); + continue; } - // Save all modified blueprints - bool bAllSaved = true; - for (UBlueprint* BP : ModifiedBlueprints) + EntryResult->SetStringField(TEXT("blueprint"), Entry.Blueprint); + EntryResult->SetStringField(TEXT("nodeId"), Entry.NodeId); + EntryResult->SetStringField(TEXT("pinName"), Entry.PinName); + + FString LoadError; + UBlueprint* BP = Helper->LoadBlueprintByName(Entry.Blueprint, LoadError); + if (!BP) { - FBlueprintEditorUtils::MarkBlueprintAsModified(BP); - FKismetEditorUtilities::CompileBlueprint(BP); - if (!SaveBlueprintPackage(BP)) + EntryResult->SetStringField(TEXT("error"), LoadError); + continue; + } + + 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"), - SuccessCount, BatchArray->Num(), bAllSaved ? TEXT("true") : TEXT("false")); + FString OldValue = Pin->DefaultValue; + Pin->DefaultValue = Entry.Value; - Result->SetBoolField(TEXT("success"), true); - Result->SetNumberField(TEXT("successCount"), SuccessCount); - Result->SetNumberField(TEXT("totalCount"), BatchArray->Num()); - Result->SetArrayField(TEXT("results"), Results); - Result->SetBoolField(TEXT("saved"), bAllSaved); - return; + EntryResult->SetBoolField(TEXT("success"), true); + EntryResult->SetStringField(TEXT("oldValue"), OldValue); + EntryResult->SetStringField(TEXT("newValue"), Pin->DefaultValue); + SuccessCount++; + ModifiedNodes.Add(Node); + ModifiedBlueprints.Add(BP); } - // Single-pin mode (existing logic) - 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()) + for (UEdGraphNode* Node : ModifiedNodes) { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, nodeId, pinName")); + Node->ReconstructNode(); } - // Load Blueprint - FString LoadError; - UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); - if (!BP) + for (UBlueprint* BP : ModifiedBlueprints) { - return MakeErrorJson(Result, LoadError); + FBlueprintEditorUtils::MarkBlueprintAsModified(BP); } - // Find node - UEdGraph* Graph = nullptr; - 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); + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SetPinDefault — %d/%d succeeded"), + SuccessCount, Pins.Array.Num()); Result->SetBoolField(TEXT("success"), true); - Result->SetStringField(TEXT("blueprint"), BlueprintName); - Result->SetStringField(TEXT("nodeId"), NodeId); - Result->SetStringField(TEXT("pinName"), PinName); - Result->SetStringField(TEXT("oldValue"), OldValue); - Result->SetStringField(TEXT("newValue"), Pin->DefaultValue); - Result->SetBoolField(TEXT("saved"), bSaved); + Result->SetNumberField(TEXT("successCount"), SuccessCount); + Result->SetNumberField(TEXT("totalCount"), Pins.Array.Num()); + Result->SetArrayField(TEXT("results"), Results); } // ============================================================ @@ -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")); - if (BlueprintName.IsEmpty()) - { - return MakeErrorJson(Result, TEXT("Missing required field: blueprint")); - } + MCPHelper* Helper = MCPHelper::Get(); - // Load Blueprint FString LoadError; - UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); + UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return Helper->MakeErrorJson(Result, LoadError); } - // Check for batch mode - const TArray>* NodesArray = nullptr; - bool bBatchMode = Json->TryGetArrayField(TEXT("nodes"), NodesArray) && NodesArray && NodesArray->Num() > 0; + TArray> Results; + int32 SuccessCount = 0; - if (bBatchMode) + for (const TSharedPtr& NodeVal : Nodes.Array) { - TArray> Results; - int32 SuccessCount = 0; + TSharedRef EntryResult = MakeShared(); + Results.Add(MakeShared(EntryResult)); - for (const TSharedPtr& NodeVal : *NodesArray) + FMoveNodeEntry Entry; + FString PopulateError = Helper->PopulateFromJson(FMoveNodeEntry::StaticStruct(), &Entry, NodeVal); + if (!PopulateError.IsEmpty()) { - TSharedPtr NodeObj = NodeVal->AsObject(); - if (!NodeObj.IsValid()) continue; - - FString NodeId = NodeObj->GetStringField(TEXT("nodeId")); - int32 X = (int32)NodeObj->GetNumberField(TEXT("x")); - int32 Y = (int32)NodeObj->GetNumberField(TEXT("y")); - - TSharedRef EntryResult = MakeShared(); - 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(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(EntryResult)); - SuccessCount++; + EntryResult->SetStringField(TEXT("error"), PopulateError); + continue; } - FBlueprintEditorUtils::MarkBlueprintAsModified(BP); - bool bSaved = SaveBlueprintPackage(BP); + EntryResult->SetStringField(TEXT("nodeId"), Entry.NodeId); - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: MoveNode batch — %d/%d succeeded, save %s"), - SuccessCount, NodesArray->Num(), bSaved ? TEXT("true") : TEXT("false")); + UEdGraphNode* Node = Helper->FindNodeByGuid(BP, Entry.NodeId); + if (!Node) + { + EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.NodeId)); + continue; + } - Result->SetBoolField(TEXT("success"), true); - Result->SetStringField(TEXT("blueprint"), BlueprintName); - Result->SetNumberField(TEXT("movedCount"), SuccessCount); - Result->SetNumberField(TEXT("totalRequested"), NodesArray->Num()); - Result->SetArrayField(TEXT("results"), Results); - Result->SetBoolField(TEXT("saved"), bSaved); - return; + int32 OldX = Node->NodePosX; + int32 OldY = Node->NodePosY; + Node->NodePosX = Entry.X; + Node->NodePosY = Entry.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); + 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); - bool bSaved = SaveBlueprintPackage(BP); + + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: MoveNode — %d/%d succeeded"), + SuccessCount, Nodes.Array.Num()); Result->SetBoolField(TEXT("success"), true); - Result->SetStringField(TEXT("blueprint"), BlueprintName); - Result->SetStringField(TEXT("nodeId"), NodeId); - Result->SetNumberField(TEXT("oldX"), OldX); - Result->SetNumberField(TEXT("oldY"), OldY); - Result->SetNumberField(TEXT("newX"), Node->NodePosX); - Result->SetNumberField(TEXT("newY"), Node->NodePosY); - Result->SetBoolField(TEXT("saved"), bSaved); + Result->SetStringField(TEXT("blueprint"), Blueprint); + Result->SetNumberField(TEXT("movedCount"), SuccessCount); + Result->SetNumberField(TEXT("totalRequested"), Nodes.Array.Num()); + Result->SetArrayField(TEXT("results"), Results); } // ============================================================ -// 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")); - FString GraphName = Json->GetStringField(TEXT("graph")); + MCPHelper* Helper = MCPHelper::Get(); - if (BlueprintName.IsEmpty() || GraphName.IsEmpty()) - { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph")); - } - - // Get node IDs - const TArray>* 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; - UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); + UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return Helper->MakeErrorJson(Result, LoadError); } // Find the target graph - FString DecodedGraphName = UrlDecode(GraphName); + FString DecodedGraphName = MCPHelper::UrlDecode(Graph); UEdGraph* TargetGraph = nullptr; TArray 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; } } 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 TArray SourceNodes; TArray NotFound; - for (const TSharedPtr& IdVal : *NodeIdsArray) + for (const TSharedPtr& IdVal : NodeIds.Array) { FString NodeId = IdVal->AsString(); - UEdGraphNode* Node = FindNodeByGuid(BP, NodeId); + UEdGraphNode* Node = Helper->FindNodeByGuid(BP, NodeId); if (Node) { - // Verify it's in the target graph if (Node->GetGraph() == TargetGraph) { SourceNodes.Add(Node); @@ -2311,11 +2147,11 @@ void FBlueprintMCPServer::HandleDuplicateNodes(const FJsonObject* Json, FJsonObj 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'"), - SourceNodes.Num(), *DecodedGraphName, *BlueprintName); + SourceNodes.Num(), *DecodedGraphName, *Blueprint); // Duplicate each node TArray> DuplicatedNodes; @@ -2323,7 +2159,6 @@ void FBlueprintMCPServer::HandleDuplicateNodes(const FJsonObject* Json, FJsonObj for (UEdGraphNode* SourceNode : SourceNodes) { - // Duplicate the node using DuplicateObject UEdGraphNode* NewNode = DuplicateObject(SourceNode, TargetGraph); if (!NewNode) { @@ -2334,15 +2169,12 @@ void FBlueprintMCPServer::HandleDuplicateNodes(const FJsonObject* Json, FJsonObj continue; } - // Assign new GUID NewNode->CreateNewGuid(); OldToNewGuidMap.Add(SourceNode->NodeGuid, NewNode->NodeGuid); - // Offset position NewNode->NodePosX += OffsetX; NewNode->NodePosY += OffsetY; - // Break all connections on the duplicate (they point to old pin instances) for (UEdGraphPin* Pin : NewNode->Pins) { if (Pin) @@ -2351,7 +2183,6 @@ void FBlueprintMCPServer::HandleDuplicateNodes(const FJsonObject* Json, FJsonObj } } - // Add to graph TargetGraph->AddNode(NewNode, false, false); TSharedRef Entry = MakeShared(); @@ -2365,17 +2196,15 @@ void FBlueprintMCPServer::HandleDuplicateNodes(const FJsonObject* Json, FJsonObj } FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - bool bSaved = SaveBlueprintPackage(BP); - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Duplicated %d node(s), save %s"), - DuplicatedNodes.Num(), bSaved ? TEXT("true") : TEXT("false")); + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Duplicated %d node(s)"), + DuplicatedNodes.Num()); Result->SetBoolField(TEXT("success"), true); - Result->SetStringField(TEXT("blueprint"), BlueprintName); + Result->SetStringField(TEXT("blueprint"), Blueprint); Result->SetStringField(TEXT("graph"), DecodedGraphName); Result->SetNumberField(TEXT("duplicatedCount"), DuplicatedNodes.Num()); Result->SetArrayField(TEXT("nodes"), DuplicatedNodes); - Result->SetBoolField(TEXT("saved"), bSaved); if (NotFound.Num() > 0) { @@ -2634,61 +2463,90 @@ void UMCPHandler_SpawnNode::Handle(const FJsonObject* Json, FJsonObject* Result) return; } - // Find the spawner by exact full name - TArray Matches = FNodeActionSearch::FindSpawner(ActionName); - 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]; + TArray> Results; + int32 SuccessCount = 0; - // Invoke the spawner - FVector2D Location(PosX, PosY); - IBlueprintNodeBinder::FBindingSet Bindings; - UEdGraphNode* NewNode = Spawner->Invoke(TargetGraph, Bindings, Location); - - if (!NewNode) + for (const TSharedPtr& NodeVal : Nodes.Array) { - return Helper->MakeErrorJson(Result, TEXT("Spawner Invoke() returned null — node creation failed.")); - } + TSharedRef EntryResult = MakeShared(); + Results.Add(MakeShared(EntryResult)); - // Ensure valid GUID - if (!NewNode->NodeGuid.IsValid()) - { - NewNode->CreateNewGuid(); + FSpawnNodeEntry Entry; + FString PopulateError = Helper->PopulateFromJson(FSpawnNodeEntry::StaticStruct(), &Entry, NodeVal); + if (!PopulateError.IsEmpty()) + { + EntryResult->SetStringField(TEXT("error"), PopulateError); + continue; + } + + EntryResult->SetStringField(TEXT("actionName"), Entry.ActionName); + + // Find the spawner by exact full name + TArray 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 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); - if (Save) Helper->SaveBlueprintPackage(BP); - 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(), - *ActionName, - *DecodedGraphName, - *Blueprint); - - // Serialize result - TSharedPtr NodeState = Helper->SerializeNode(NewNode); + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SpawnNode — %d/%d succeeded in graph '%s' of '%s'"), + SuccessCount, Nodes.Array.Num(), *DecodedGraphName, *Blueprint); Result->SetBoolField(TEXT("success"), true); Result->SetStringField(TEXT("blueprint"), Blueprint); Result->SetStringField(TEXT("graph"), DecodedGraphName); - Result->SetStringField(TEXT("actionName"), ActionName); - Result->SetStringField(TEXT("nodeId"), NewNode->NodeGuid.ToString()); - Result->SetStringField(TEXT("nodeClass"), NewNode->GetClass()->GetName()); - Result->SetStringField(TEXT("nodeTitle"), NewNode->GetNodeTitle(ENodeTitleType::ListView).ToString()); - if (NodeState.IsValid()) - { - Result->SetObjectField(TEXT("node"), NodeState); - } + Result->SetNumberField(TEXT("successCount"), SuccessCount); + Result->SetNumberField(TEXT("totalCount"), Nodes.Array.Num()); + Result->SetArrayField(TEXT("results"), Results); } diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPServer.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPServer.cpp index ecd536f7..8dd6e15b 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPServer.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPServer.cpp @@ -1,5 +1,6 @@ #include "BlueprintMCPServer.h" #include "MCPHandler.h" +#include "UObject/StrongObjectPtr.h" #include "Materials/MaterialExpression.h" #include "AssetRegistry/AssetRegistryModule.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, QueuedHandler(TEXT("refreshAllNodes"))); 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, - QueuedHandler(TEXT("moveNode"))); + QueuedHandler(TEXT("move_node"))); Router->BindRoute(FHttpPath(TEXT("/api/get-node-comment")), EHttpServerRequestVerbs::VERB_POST, QueuedHandler(TEXT("getNodeComment"))); 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, QueuedHandler(TEXT("deleteNode"))); 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, QueuedHandler(TEXT("searchByType"))); 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))); } - UMCPHandler* Handler = NewObject(GetTransientPackage(), *HandlerClass); - if (PopulateHandlerFromJson(Handler, Params.Get(), &*Result)) + TStrongObjectPtr Handler(NewObject(GetTransientPackage(), *HandlerClass)); + FString PopulateError = PopulateFromJson(Handler->GetClass(), Handler.Get(), Params.Get()); + if (PopulateError.IsEmpty()) { Handler->Handle(Params.Get(), &*Result); } + else + { + MakeErrorJson(&*Result, PopulateError); + } if (bIsMutation && GEditor) { @@ -979,11 +985,11 @@ void FBlueprintMCPServer::RegisterHandlers() TEXT("connectPins"), TEXT("disconnectPin"), TEXT("refreshAllNodes"), - TEXT("setPinDefault"), - TEXT("moveNode"), + TEXT("set_pin_default"), + TEXT("move_node"), TEXT("changeStructNodeType"), TEXT("deleteNode"), - TEXT("duplicateNodes"), + TEXT("duplicate_nodes"), TEXT("addNode"), TEXT("spawn_node"), TEXT("setNodeComment"), @@ -1052,8 +1058,8 @@ void FBlueprintMCPServer::RegisterHandlers() H(TEXT("connectPins"), &FBlueprintMCPServer::HandleConnectPins); H(TEXT("disconnectPin"), &FBlueprintMCPServer::HandleDisconnectPin); H(TEXT("refreshAllNodes"), &FBlueprintMCPServer::HandleRefreshAllNodes); - H(TEXT("setPinDefault"), &FBlueprintMCPServer::HandleSetPinDefault); - H(TEXT("moveNode"), &FBlueprintMCPServer::HandleMoveNode); + // set_pin_default is now handled by UMCPHandler_SetPinDefault (new-style registry) + // move_node is now handled by UMCPHandler_MoveNode (new-style registry) H(TEXT("getNodeComment"), &FBlueprintMCPServer::HandleGetNodeComment); H(TEXT("setNodeComment"), &FBlueprintMCPServer::HandleSetNodeComment); H(TEXT("getPinInfo"), &FBlueprintMCPServer::HandleGetPinInfo); @@ -1063,7 +1069,7 @@ void FBlueprintMCPServer::RegisterHandlers() H(TEXT("listProperties"), &FBlueprintMCPServer::HandleListProperties); H(TEXT("changeStructNodeType"), &FBlueprintMCPServer::HandleChangeStructNodeType); 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("validateAllBlueprints"), &FBlueprintMCPServer::HandleValidateAllBlueprints); H(TEXT("addNode"), &FBlueprintMCPServer::HandleAddNode); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlerPopulate.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlerPopulate.cpp index b45121ab..62703f74 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlerPopulate.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlerPopulate.cpp @@ -10,12 +10,12 @@ namespace MCPPopulate // 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. FString SetPropertyFromJson( - UMCPHandler* Handler, + void* Container, FProperty* Prop, const FString& FieldName, const FJsonObject* Json) { - void* ValuePtr = Prop->ContainerPtrToValuePtr(Handler); + void* ValuePtr = Prop->ContainerPtrToValuePtr(Container); // FString if (FStrProperty* StrProp = CastField(Prop)) @@ -118,17 +118,29 @@ FString SetPropertyFromJson( return FString(); } - // FMCPSubtree — stash the JSON subtree into the struct + // FMCPJsonObject — stash a JSON object into the struct if (FStructProperty* StructProp = CastField(Prop)) { - if (StructProp->Struct == FMCPSubtree::StaticStruct()) + if (StructProp->Struct == FMCPJsonObject::StaticStruct()) { if (!Json->HasTypedField(FieldName)) { return FString::Printf(TEXT("'%s' must be an object"), *FieldName); } - FMCPSubtree* Subtree = StructProp->ContainerPtrToValuePtr(Handler); - Subtree->Json = Json->GetObjectField(FieldName); + FMCPJsonObject* Obj = StructProp->ContainerPtrToValuePtr(Container); + Obj->Json = Json->GetObjectField(FieldName); + return FString(); + } + + // FMCPJsonArray — stash a JSON array into the struct + if (StructProp->Struct == FMCPJsonArray::StaticStruct()) + { + if (!Json->HasTypedField(FieldName)) + { + return FString::Printf(TEXT("'%s' must be an array"), *FieldName); + } + FMCPJsonArray* Arr = StructProp->ContainerPtrToValuePtr(Container); + Arr->Array = Json->GetArrayField(FieldName); return FString(); } } @@ -152,18 +164,28 @@ FString PropertyNameToJsonKey(const FString& PropName) } // namespace MCPPopulate -bool FBlueprintMCPServer::PopulateHandlerFromJson( - UMCPHandler* Handler, - const FJsonObject* Json, - FJsonObject* Result) +FString FBlueprintMCPServer::PopulateFromJson( + UStruct* StructType, + void* Container, + const TSharedPtr& 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. TSet KnownKeys; TArray Properties; - for (TFieldIterator It(HandlerClass, EFieldIterationFlags::None); It; ++It) + for (TFieldIterator It(StructType, EFieldIterationFlags::None); It; ++It) { FProperty* Prop = *It; Properties.Add(Prop); @@ -175,18 +197,7 @@ bool FBlueprintMCPServer::PopulateHandlerFromJson( { if (!KnownKeys.Contains(KV.Key)) { - MakeErrorJson(Result, FString::Printf( - TEXT("Unknown parameter '%s'"), *KV.Key)); - Result->SetArrayField(TEXT("validParameters"), - [&]() { - TArray> Arr; - for (const FString& Key : KnownKeys) - { - Arr.Add(MakeShared(Key)); - } - return Arr; - }()); - return false; + return FString::Printf(TEXT("Unknown parameter '%s'"), *KV.Key); } } @@ -200,20 +211,17 @@ bool FBlueprintMCPServer::PopulateHandlerFromJson( { if (!bOptional) { - MakeErrorJson(Result, FString::Printf( - TEXT("Missing required parameter '%s'"), *JsonKey)); - return false; + return FString::Printf(TEXT("Missing required parameter '%s'"), *JsonKey); } continue; } - FString Error = MCPPopulate::SetPropertyFromJson(Handler, Prop, JsonKey, Json); + FString Error = MCPPopulate::SetPropertyFromJson(Container, Prop, JsonKey, Json); if (!Error.IsEmpty()) { - MakeErrorJson(Result, Error); - return false; + return Error; } } - return true; + return FString(); } diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintMCPHandlers_Interfaces.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintMCPHandlers_Interfaces.h index e124f8ff..89ee7c8d 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintMCPHandlers_Interfaces.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintMCPHandlers_Interfaces.h @@ -34,9 +34,6 @@ public: UPROPERTY(meta=(Description="Interface name (e.g. 'BPI_MyInterface') or native UInterface class name")) FString InterfaceName; - UPROPERTY(meta=(Optional, Description="Save the blueprint after adding the interface")) - bool Save = false; - virtual FString GetDescription() const override { 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")) bool PreserveFunctions = false; - UPROPERTY(meta=(Optional, Description="Save the blueprint after removing the interface")) - bool Save = false; - virtual FString GetDescription() const override { return TEXT("Remove a Blueprint Interface implementation from a Blueprint. " diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintMCPHandlers_Mutation.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintMCPHandlers_Mutation.h index b8961cb5..8222e6f6 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintMCPHandlers_Mutation.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintMCPHandlers_Mutation.h @@ -4,6 +4,122 @@ #include "MCPHandler.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")) class UMCPHandler_SpawnNode : public UMCPHandler { @@ -16,23 +132,13 @@ public: UPROPERTY(meta=(Description="Graph name (e.g. 'EventGraph')")) FString Graph; - UPROPERTY(meta=(Description="Full action name from search_node_actions (e.g. 'Luprex|Lua|Read Lua Values')")) - FString ActionName; - - 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; + UPROPERTY(meta=(Description="Array of {actionName, posX, posY} objects. Use search_node_actions to find action names.")) + FMCPJsonArray Nodes; virtual FString GetDescription() const override { - return TEXT("Create a node 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 " - "ANY node type that appears in the editor's right-click menu, including custom K2 nodes. " + return TEXT("Create nodes in a Blueprint graph using the editor's action database. " + "Can create 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."); } diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintMCPServer.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintMCPServer.h index 343a857f..1a267032 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintMCPServer.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintMCPServer.h @@ -142,7 +142,6 @@ private: void HandleRemoveFunctionParameter(const FJsonObject* Json, FJsonObject* Result); void HandleDeleteAsset(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 HandleRenameAsset(const FJsonObject* Json, FJsonObject* Result); @@ -154,8 +153,6 @@ private: void HandleConnectPins(const FJsonObject* Json, FJsonObject* Result); void HandleDisconnectPin(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 HandleSetNodeComment(const FJsonObject* Json, FJsonObject* Result); @@ -297,9 +294,10 @@ public: static void CopyJsonFields(const FJsonObject* Source, FJsonObject* Dest); static FString UrlDecode(const FString& EncodedString); - // Populate a handler's UPROPERTY fields from JSON. - // Returns true on success, or sets error on Result and returns false. - bool PopulateHandlerFromJson(UMCPHandler* Handler, const FJsonObject* Json, FJsonObject* Result); + // Populate UPROPERTY fields of a UStruct (or UClass) instance from JSON. + // Returns empty string on success, or an error message on failure. + FString PopulateFromJson(UStruct* StructType, void* Container, const TSharedPtr& JsonValue); + FString PopulateFromJson(UStruct* StructType, void* Container, const FJsonObject* Json); // ----- Material helpers ----- /** Ensure that Material->MaterialGraph exists (creates it on demand for commandlet mode). */ diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPHandler.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPHandler.h index 03e743bc..aecaed57 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPHandler.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPHandler.h @@ -5,17 +5,24 @@ #include "Dom/JsonObject.h" #include "MCPHandler.generated.h" -// Marker struct for handler parameters that accept a JSON subtree. -// The parameter name is included in the tool schema as "type": "object". -// PopulateHandlerFromJson stashes the actual JSON object into the Json field, -// so the handler can read it directly without parsing from the raw request. +// Marker struct for handler parameters that accept a JSON object. +// PopulateFromJson stashes the actual JSON object into the Json field. USTRUCT() -struct FMCPSubtree +struct FMCPJsonObject { GENERATED_BODY() TSharedPtr 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> Array; +}; + // Base class for self-registering MCP tool handlers. // // Subclasses declare their parameters as UPROPERTY fields, which are