From 7351756a71713e2dcdcd5c6497d2094fdf3c30db Mon Sep 17 00:00:00 2001 From: jyelon Date: Fri, 6 Mar 2026 03:14:14 -0500 Subject: [PATCH] Another batch of MCP handlers ported --- .../Private/BlueprintMCPHandlers_Mutation.cpp | 408 ++++++++---------- .../Private/BlueprintMCPServer.cpp | 28 +- .../Public/BlueprintMCPHandlers_Mutation.h | 141 ++++++ .../BlueprintMCP/Public/BlueprintMCPServer.h | 5 - 4 files changed, 341 insertions(+), 241 deletions(-) diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Mutation.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Mutation.cpp index 43ef061f..1aac2c26 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Mutation.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Mutation.cpp @@ -459,219 +459,212 @@ void FBlueprintMCPServer::HandleDeleteAsset(const FJsonObject* Json, FJsonObject // HandleConnectPins — wire two pins together // ============================================================ -void FBlueprintMCPServer::HandleConnectPins(const FJsonObject* Json, FJsonObject* Result) +// connect_pins is now handled by UMCPHandler_ConnectPins (new-style registry) + +void UMCPHandler_ConnectPins::Handle(const FJsonObject* Json, FJsonObject* Result) { - FString BlueprintName = Json->GetStringField(TEXT("blueprint")); - FString SourceNodeId = Json->GetStringField(TEXT("sourceNodeId")); - FString SourcePinName = Json->GetStringField(TEXT("sourcePinName")); - FString TargetNodeId = Json->GetStringField(TEXT("targetNodeId")); - FString TargetPinName = Json->GetStringField(TEXT("targetPinName")); + MCPHelper* Helper = MCPHelper::Get(); - if (BlueprintName.IsEmpty() || SourceNodeId.IsEmpty() || SourcePinName.IsEmpty() || - TargetNodeId.IsEmpty() || TargetPinName.IsEmpty()) - { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, sourceNodeId, sourcePinName, targetNodeId, targetPinName")); - } - - // 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 source node - UEdGraph* SourceGraph = nullptr; - UEdGraphNode* SourceNode = FindNodeByGuid(BP, SourceNodeId, &SourceGraph); - if (!SourceNode) - { - return MakeErrorJson(Result, FString::Printf(TEXT("Source node '%s' not found"), *SourceNodeId)); - } + TArray> Results; + int32 SuccessCount = 0; - // Find target node - UEdGraphNode* TargetNode = FindNodeByGuid(BP, TargetNodeId); - if (!TargetNode) + for (const TSharedPtr& ConnVal : Connections.Array) { - return MakeErrorJson(Result, FString::Printf(TEXT("Target node '%s' not found"), *TargetNodeId)); - } + TSharedRef EntryResult = MakeShared(); + Results.Add(MakeShared(EntryResult)); - // Find source pin - UEdGraphPin* SourcePin = SourceNode->FindPin(FName(*SourcePinName)); - if (!SourcePin) - { - // List available pins for debugging - TArray> PinNames; - for (UEdGraphPin* P : SourceNode->Pins) + FConnectPinsEntry Entry; + FString PopulateError = Helper->PopulateFromJson(FConnectPinsEntry::StaticStruct(), &Entry, ConnVal); + if (!PopulateError.IsEmpty()) { - if (P) PinNames.Add(MakeShared( - FString::Printf(TEXT("%s (%s)"), *P->PinName.ToString(), - P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output")))); + EntryResult->SetStringField(TEXT("error"), PopulateError); + continue; } - MakeErrorJson(Result, FString::Printf(TEXT("Source pin '%s' not found on node '%s'"), - *SourcePinName, *SourceNodeId)); - Result->SetArrayField(TEXT("availablePins"), PinNames); - return; - } - // Find target pin - UEdGraphPin* TargetPin = TargetNode->FindPin(FName(*TargetPinName)); - if (!TargetPin) - { - // List available pins for debugging - TArray> PinNames; - for (UEdGraphPin* P : TargetNode->Pins) + EntryResult->SetStringField(TEXT("sourceNodeId"), Entry.SourceNodeId); + EntryResult->SetStringField(TEXT("sourcePinName"), Entry.SourcePinName); + EntryResult->SetStringField(TEXT("targetNodeId"), Entry.TargetNodeId); + EntryResult->SetStringField(TEXT("targetPinName"), Entry.TargetPinName); + + UEdGraph* SourceGraph = nullptr; + UEdGraphNode* SourceNode = Helper->FindNodeByGuid(BP, Entry.SourceNodeId, &SourceGraph); + if (!SourceNode) { - if (P) PinNames.Add(MakeShared( - FString::Printf(TEXT("%s (%s)"), *P->PinName.ToString(), - P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output")))); + EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Source node '%s' not found"), *Entry.SourceNodeId)); + continue; } - MakeErrorJson(Result, FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), - *TargetPinName, *TargetNodeId)); - Result->SetArrayField(TEXT("availablePins"), PinNames); - return; + + UEdGraphNode* TargetNode = Helper->FindNodeByGuid(BP, Entry.TargetNodeId); + if (!TargetNode) + { + EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target node '%s' not found"), *Entry.TargetNodeId)); + continue; + } + + UEdGraphPin* SourcePin = SourceNode->FindPin(FName(*Entry.SourcePinName)); + if (!SourcePin) + { + EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Source pin '%s' not found on node '%s'"), *Entry.SourcePinName, *Entry.SourceNodeId)); + continue; + } + + UEdGraphPin* TargetPin = TargetNode->FindPin(FName(*Entry.TargetPinName)); + if (!TargetPin) + { + EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), *Entry.TargetPinName, *Entry.TargetNodeId)); + continue; + } + + const UEdGraphSchema* Schema = SourceGraph->GetSchema(); + if (!Schema) + { + EntryResult->SetStringField(TEXT("error"), TEXT("Graph schema not found")); + continue; + } + + bool bConnected = Schema->TryCreateConnection(SourcePin, TargetPin); + if (!bConnected) + { + EntryResult->SetStringField(TEXT("error"), FString::Printf( + TEXT("Cannot connect %s (%s) to %s (%s) — types are incompatible"), + *Entry.SourcePinName, *SourcePin->PinType.PinCategory.ToString(), + *Entry.TargetPinName, *TargetPin->PinType.PinCategory.ToString())); + continue; + } + + EntryResult->SetBoolField(TEXT("success"), true); + SuccessCount++; } - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Connecting %s.%s -> %s.%s"), - *SourceNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString(), *SourcePinName, - *TargetNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString(), *TargetPinName); - - // Try type-validated connection via the schema - const UEdGraphSchema* Schema = SourceGraph->GetSchema(); - if (!Schema) + if (SuccessCount > 0) { - return MakeErrorJson(Result, TEXT("Graph schema not found")); - } - bool bConnected = Schema->TryCreateConnection(SourcePin, TargetPin); - - Result->SetBoolField(TEXT("success"), bConnected); - Result->SetStringField(TEXT("blueprint"), BlueprintName); - Result->SetStringField(TEXT("sourcePinType"), SourcePin->PinType.PinCategory.ToString()); - if (SourcePin->PinType.PinSubCategoryObject.IsValid()) - Result->SetStringField(TEXT("sourcePinSubtype"), SourcePin->PinType.PinSubCategoryObject->GetName()); - Result->SetStringField(TEXT("targetPinType"), TargetPin->PinType.PinCategory.ToString()); - if (TargetPin->PinType.PinSubCategoryObject.IsValid()) - Result->SetStringField(TEXT("targetPinSubtype"), TargetPin->PinType.PinSubCategoryObject->GetName()); - - if (!bConnected) - { - // Provide type mismatch details - FString Reason = FString::Printf(TEXT("Cannot connect %s (%s) to %s (%s) — types are incompatible"), - *SourcePinName, *SourcePin->PinType.PinCategory.ToString(), - *TargetPinName, *TargetPin->PinType.PinCategory.ToString()); - return MakeErrorJson(Result, Reason); + FBlueprintEditorUtils::MarkBlueprintAsModified(BP); } - // Save - bool bSaved = SaveBlueprintPackage(BP); - Result->SetBoolField(TEXT("saved"), bSaved); + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: ConnectPins — %d/%d succeeded in '%s'"), + SuccessCount, Connections.Array.Num(), *Blueprint); - // Return updated node state for both source and target - TSharedPtr SourceNodeState = SerializeNode(SourceNode); - TSharedPtr TargetNodeState = SerializeNode(TargetNode); - if (SourceNodeState.IsValid()) - Result->SetObjectField(TEXT("updatedSourceNode"), SourceNodeState); - if (TargetNodeState.IsValid()) - Result->SetObjectField(TEXT("updatedTargetNode"), TargetNodeState); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Connection %s, save %s"), - bConnected ? TEXT("succeeded") : TEXT("failed"), - bSaved ? TEXT("succeeded") : TEXT("failed")); + Result->SetBoolField(TEXT("success"), true); + Result->SetStringField(TEXT("blueprint"), Blueprint); + Result->SetNumberField(TEXT("successCount"), SuccessCount); + Result->SetNumberField(TEXT("totalCount"), Connections.Array.Num()); + Result->SetArrayField(TEXT("results"), Results); } // ============================================================ // HandleDisconnectPin — break connections on a pin // ============================================================ -void FBlueprintMCPServer::HandleDisconnectPin(const FJsonObject* Json, FJsonObject* Result) +// disconnect_pin is now handled by UMCPHandler_DisconnectPin (new-style registry) + +void UMCPHandler_DisconnectPin::Handle(const FJsonObject* Json, FJsonObject* Result) { - FString BlueprintName = Json->GetStringField(TEXT("blueprint")); - FString NodeId = Json->GetStringField(TEXT("nodeId")); - FString PinName = Json->GetStringField(TEXT("pinName")); + MCPHelper* Helper = MCPHelper::Get(); - if (BlueprintName.IsEmpty() || NodeId.IsEmpty() || PinName.IsEmpty()) - { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, nodeId, pinName")); - } - - // Optional: specific target to disconnect from - FString TargetNodeId = Json->GetStringField(TEXT("targetNodeId")); - FString TargetPinName = Json->GetStringField(TEXT("targetPinName")); - - // 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 source node - UEdGraphNode* Node = FindNodeByGuid(BP, NodeId); - if (!Node) - { - return MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId)); - } + TArray> Results; + int32 SuccessCount = 0; + int32 TotalDisconnected = 0; - // Find pin - UEdGraphPin* Pin = Node->FindPin(FName(*PinName)); - if (!Pin) + for (const TSharedPtr& DiscVal : Disconnections.Array) { - return MakeErrorJson(Result, FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *PinName, *NodeId)); - } + TSharedRef EntryResult = MakeShared(); + Results.Add(MakeShared(EntryResult)); - int32 DisconnectedCount = 0; - - if (!TargetNodeId.IsEmpty() && !TargetPinName.IsEmpty()) - { - // Disconnect a single specific link - UEdGraphNode* TargetNode = FindNodeByGuid(BP, TargetNodeId); - if (!TargetNode) + FDisconnectPinEntry Entry; + FString PopulateError = Helper->PopulateFromJson(FDisconnectPinEntry::StaticStruct(), &Entry, DiscVal); + if (!PopulateError.IsEmpty()) { - return MakeErrorJson(Result, FString::Printf(TEXT("Target node '%s' not found"), *TargetNodeId)); + EntryResult->SetStringField(TEXT("error"), PopulateError); + continue; } - UEdGraphPin* TargetPin = TargetNode->FindPin(FName(*TargetPinName)); - if (!TargetPin) + EntryResult->SetStringField(TEXT("nodeId"), Entry.NodeId); + EntryResult->SetStringField(TEXT("pinName"), Entry.PinName); + + UEdGraphNode* Node = Helper->FindNodeByGuid(BP, Entry.NodeId); + if (!Node) { - return MakeErrorJson(Result, FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), - *TargetPinName, *TargetNodeId)); + EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.NodeId)); + continue; } - if (Pin->LinkedTo.Contains(TargetPin)) + 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; + } + + int32 DisconnectedCount = 0; + + if (!Entry.TargetNodeId.IsEmpty() && !Entry.TargetPinName.IsEmpty()) + { + UEdGraphNode* TargetNode = Helper->FindNodeByGuid(BP, Entry.TargetNodeId); + if (!TargetNode) + { + EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target node '%s' not found"), *Entry.TargetNodeId)); + continue; + } + + UEdGraphPin* TargetPin = TargetNode->FindPin(FName(*Entry.TargetPinName)); + if (!TargetPin) + { + EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), *Entry.TargetPinName, *Entry.TargetNodeId)); + continue; + } + + if (!Pin->LinkedTo.Contains(TargetPin)) + { + EntryResult->SetStringField(TEXT("error"), TEXT("The specified pins are not connected to each other")); + continue; + } + Pin->BreakLinkTo(TargetPin); DisconnectedCount = 1; } else { - return MakeErrorJson(Result, TEXT("The specified pins are not connected to each other")); - } - } - else - { - // Disconnect all links on this pin - DisconnectedCount = Pin->LinkedTo.Num(); - if (DisconnectedCount > 0) - { - Pin->BreakAllPinLinks(true); + DisconnectedCount = Pin->LinkedTo.Num(); + if (DisconnectedCount > 0) + { + Pin->BreakAllPinLinks(true); + } } + + EntryResult->SetBoolField(TEXT("success"), true); + EntryResult->SetNumberField(TEXT("disconnectedCount"), DisconnectedCount); + SuccessCount++; + TotalDisconnected += DisconnectedCount; } - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Disconnected %d link(s) from %s.%s"), - DisconnectedCount, *Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString(), *PinName); - - // Save - bool bSaved = false; - if (DisconnectedCount > 0) + if (TotalDisconnected > 0) { - bSaved = SaveBlueprintPackage(BP); + FBlueprintEditorUtils::MarkBlueprintAsModified(BP); } + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: DisconnectPin — %d/%d succeeded, %d links broken in '%s'"), + SuccessCount, Disconnections.Array.Num(), TotalDisconnected, *Blueprint); + Result->SetBoolField(TEXT("success"), true); - Result->SetStringField(TEXT("blueprint"), BlueprintName); - Result->SetNumberField(TEXT("disconnectedCount"), DisconnectedCount); - Result->SetBoolField(TEXT("saved"), bSaved); + Result->SetStringField(TEXT("blueprint"), Blueprint); + Result->SetNumberField(TEXT("successCount"), SuccessCount); + Result->SetNumberField(TEXT("totalCount"), Disconnections.Array.Num()); + Result->SetNumberField(TEXT("totalDisconnected"), TotalDisconnected); + Result->SetArrayField(TEXT("results"), Results); } // ============================================================ @@ -1128,34 +1121,28 @@ void FBlueprintMCPServer::HandleChangeStructNodeType(const FJsonObject* Json, FJ // HandleDeleteNode — remove a node from a blueprint graph // ============================================================ -void FBlueprintMCPServer::HandleDeleteNode(const FJsonObject* Json, FJsonObject* Result) +// delete_node is now handled by UMCPHandler_DeleteNode (new-style registry) + +void UMCPHandler_DeleteNode::Handle(const FJsonObject* Json, FJsonObject* Result) { - FString BlueprintName = Json->GetStringField(TEXT("blueprint")); - FString NodeId = Json->GetStringField(TEXT("nodeId")); + MCPHelper* Helper = MCPHelper::Get(); - if (BlueprintName.IsEmpty() || NodeId.IsEmpty()) - { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, nodeId")); - } - - // 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 node UEdGraph* Graph = nullptr; - UEdGraphNode* Node = FindNodeByGuid(BP, NodeId, &Graph); + UEdGraphNode* Node = Helper->FindNodeByGuid(BP, NodeId, &Graph); if (!Node) { - return MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId)); + return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId)); } if (!Graph) { - return MakeErrorJson(Result, FString::Printf(TEXT("Graph not found for node '%s'"), *NodeId)); + return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Graph not found for node '%s'"), *NodeId)); } FString NodeClass = Node->GetClass()->GetName(); @@ -1167,7 +1154,7 @@ void FBlueprintMCPServer::HandleDeleteNode(const FJsonObject* Json, FJsonObject* // without recreating the entire function/event. if (Cast(Node)) { - return MakeErrorJson(Result, FString::Printf( + return Helper->MakeErrorJson(Result, FString::Printf( TEXT("Cannot delete FunctionEntry node '%s' in graph '%s'. ") TEXT("This is the root node of the function — removing it would leave an empty, uncompilable graph. ") TEXT("To remove the entire function, delete it from the Blueprint editor."), @@ -1175,41 +1162,35 @@ void FBlueprintMCPServer::HandleDeleteNode(const FJsonObject* Json, FJsonObject* } if (Cast(Node)) { - return MakeErrorJson(Result, FString::Printf( + return Helper->MakeErrorJson(Result, FString::Printf( TEXT("Cannot delete event entry node '%s' in graph '%s'. ") TEXT("This is the root node of the event handler — removing it would leave an empty, uncompilable graph."), *NodeTitle, *GraphName)); } if (Cast(Node)) { - return MakeErrorJson(Result, FString::Printf( + return Helper->MakeErrorJson(Result, FString::Printf( TEXT("Cannot delete CustomEvent entry node '%s' in graph '%s'. ") TEXT("This is the root node of the custom event — removing it would leave an empty, uncompilable graph."), *NodeTitle, *GraphName)); } UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleting node '%s' (%s) from graph '%s' in '%s'"), - *NodeId, *NodeTitle, *GraphName, *BlueprintName); + *NodeId, *NodeTitle, *GraphName, *Blueprint); - // Disconnect all pins Node->BreakAllNodeLinks(); - - // Remove the node from the graph Graph->RemoveNode(Node); - // Save (which also compiles) - bool bSaved = SaveBlueprintPackage(BP); + FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Node deleted, save %s"), - bSaved ? TEXT("succeeded") : TEXT("failed")); + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Node deleted")); Result->SetBoolField(TEXT("success"), true); - Result->SetStringField(TEXT("blueprint"), BlueprintName); + Result->SetStringField(TEXT("blueprint"), Blueprint); Result->SetStringField(TEXT("nodeId"), NodeId); Result->SetStringField(TEXT("nodeClass"), NodeClass); Result->SetStringField(TEXT("nodeTitle"), NodeTitle); Result->SetStringField(TEXT("graph"), GraphName); - Result->SetBoolField(TEXT("saved"), bSaved); } // ============================================================ @@ -2221,31 +2202,27 @@ void UMCPHandler_DuplicateNodes::Handle(const FJsonObject* Json, FJsonObject* Re // HandleGetNodeComment — read a node's comment text // ============================================================ -void FBlueprintMCPServer::HandleGetNodeComment(const FJsonObject* Json, FJsonObject* Result) -{ - FString BlueprintName = Json->GetStringField(TEXT("blueprint")); - FString NodeId = Json->GetStringField(TEXT("nodeId")); +// get_node_comment is now handled by UMCPHandler_GetNodeComment (new-style registry) - if (BlueprintName.IsEmpty() || NodeId.IsEmpty()) - { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, nodeId")); - } +void UMCPHandler_GetNodeComment::Handle(const FJsonObject* Json, FJsonObject* Result) +{ + MCPHelper* Helper = MCPHelper::Get(); FString LoadError; - UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); + UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return Helper->MakeErrorJson(Result, LoadError); } - UEdGraphNode* Node = FindNodeByGuid(BP, NodeId); + UEdGraphNode* Node = Helper->FindNodeByGuid(BP, NodeId); if (!Node) { - return MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId)); + return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId)); } Result->SetBoolField(TEXT("success"), true); - Result->SetStringField(TEXT("blueprint"), BlueprintName); + Result->SetStringField(TEXT("blueprint"), Blueprint); Result->SetStringField(TEXT("nodeId"), NodeId); Result->SetStringField(TEXT("comment"), Node->NodeComment); Result->SetBoolField(TEXT("commentBubbleVisible"), Node->bCommentBubbleVisible); @@ -2255,34 +2232,23 @@ void FBlueprintMCPServer::HandleGetNodeComment(const FJsonObject* Json, FJsonObj // HandleSetNodeComment — set a node's comment text // ============================================================ -void FBlueprintMCPServer::HandleSetNodeComment(const FJsonObject* Json, FJsonObject* Result) +// set_node_comment is now handled by UMCPHandler_SetNodeComment (new-style registry) + +void UMCPHandler_SetNodeComment::Handle(const FJsonObject* Json, FJsonObject* Result) { - FString BlueprintName = Json->GetStringField(TEXT("blueprint")); - FString NodeId = Json->GetStringField(TEXT("nodeId")); - - if (BlueprintName.IsEmpty() || NodeId.IsEmpty()) - { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, nodeId")); - } - - if (!Json->HasField(TEXT("comment"))) - { - return MakeErrorJson(Result, TEXT("Missing required field: comment")); - } - - FString Comment = Json->GetStringField(TEXT("comment")); + MCPHelper* Helper = MCPHelper::Get(); FString LoadError; - UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); + UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return Helper->MakeErrorJson(Result, LoadError); } - UEdGraphNode* Node = FindNodeByGuid(BP, NodeId); + UEdGraphNode* Node = Helper->FindNodeByGuid(BP, NodeId); if (!Node) { - return MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId)); + return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId)); } FString OldComment = Node->NodeComment; @@ -2296,17 +2262,15 @@ void FBlueprintMCPServer::HandleSetNodeComment(const FJsonObject* Json, FJsonObj } FBlueprintEditorUtils::MarkBlueprintAsModified(BP); - bool bSaved = SaveBlueprintPackage(BP); - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Set comment on node '%s' in '%s', save %s"), - *NodeId, *BlueprintName, bSaved ? TEXT("succeeded") : TEXT("failed")); + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Set comment on node '%s' in '%s'"), + *NodeId, *Blueprint); Result->SetBoolField(TEXT("success"), true); - Result->SetStringField(TEXT("blueprint"), BlueprintName); + Result->SetStringField(TEXT("blueprint"), Blueprint); Result->SetStringField(TEXT("nodeId"), NodeId); Result->SetStringField(TEXT("oldComment"), OldComment); Result->SetStringField(TEXT("newComment"), Comment); - Result->SetBoolField(TEXT("saved"), bSaved); } // ============================================================ diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPServer.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPServer.cpp index 8dd6e15b..cfe0f028 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPServer.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPServer.cpp @@ -611,9 +611,9 @@ bool FBlueprintMCPServer::Start(int32 InPort, bool bEditorMode) Router->BindRoute(FHttpPath(TEXT("/api/test-save")), EHttpServerRequestVerbs::VERB_GET, QueuedHandler(TEXT("testSave"))); Router->BindRoute(FHttpPath(TEXT("/api/connect-pins")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("connectPins"))); + QueuedHandler(TEXT("connect_pins"))); Router->BindRoute(FHttpPath(TEXT("/api/disconnect-pin")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("disconnectPin"))); + QueuedHandler(TEXT("disconnect_pin"))); 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, @@ -621,9 +621,9 @@ bool FBlueprintMCPServer::Start(int32 InPort, bool bEditorMode) Router->BindRoute(FHttpPath(TEXT("/api/move-node")), EHttpServerRequestVerbs::VERB_POST, QueuedHandler(TEXT("move_node"))); Router->BindRoute(FHttpPath(TEXT("/api/get-node-comment")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("getNodeComment"))); + QueuedHandler(TEXT("get_node_comment"))); Router->BindRoute(FHttpPath(TEXT("/api/set-node-comment")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("setNodeComment"))); + QueuedHandler(TEXT("set_node_comment"))); Router->BindRoute(FHttpPath(TEXT("/api/get-pin-info")), EHttpServerRequestVerbs::VERB_POST, QueuedHandler(TEXT("getPinInfo"))); Router->BindRoute(FHttpPath(TEXT("/api/check-pin-compatibility")), EHttpServerRequestVerbs::VERB_POST, @@ -639,7 +639,7 @@ bool FBlueprintMCPServer::Start(int32 InPort, bool bEditorMode) Router->BindRoute(FHttpPath(TEXT("/api/remove-function-parameter")), EHttpServerRequestVerbs::VERB_POST, QueuedHandler(TEXT("removeFunctionParameter"))); Router->BindRoute(FHttpPath(TEXT("/api/delete-node")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("deleteNode"))); + QueuedHandler(TEXT("delete_node"))); Router->BindRoute(FHttpPath(TEXT("/api/duplicate-nodes")), EHttpServerRequestVerbs::VERB_POST, QueuedHandler(TEXT("duplicate_nodes"))); Router->BindRoute(FHttpPath(TEXT("/api/search-by-type")), EHttpServerRequestVerbs::VERB_GET, @@ -982,17 +982,17 @@ void FBlueprintMCPServer::RegisterHandlers() TEXT("changeFunctionParamType"), TEXT("removeFunctionParameter"), TEXT("deleteAsset"), - TEXT("connectPins"), - TEXT("disconnectPin"), + TEXT("connect_pins"), + TEXT("disconnect_pin"), TEXT("refreshAllNodes"), TEXT("set_pin_default"), TEXT("move_node"), TEXT("changeStructNodeType"), - TEXT("deleteNode"), + TEXT("delete_node"), TEXT("duplicate_nodes"), TEXT("addNode"), TEXT("spawn_node"), - TEXT("setNodeComment"), + TEXT("set_node_comment"), TEXT("renameAsset"), TEXT("reparentBlueprint"), TEXT("setBlueprintDefault"), @@ -1055,20 +1055,20 @@ void FBlueprintMCPServer::RegisterHandlers() H(TEXT("changeFunctionParamType"), &FBlueprintMCPServer::HandleChangeFunctionParamType); H(TEXT("removeFunctionParameter"), &FBlueprintMCPServer::HandleRemoveFunctionParameter); H(TEXT("deleteAsset"), &FBlueprintMCPServer::HandleDeleteAsset); - H(TEXT("connectPins"), &FBlueprintMCPServer::HandleConnectPins); - H(TEXT("disconnectPin"), &FBlueprintMCPServer::HandleDisconnectPin); + // connect_pins is now handled by UMCPHandler_ConnectPins (new-style registry) + // disconnect_pin is now handled by UMCPHandler_DisconnectPin (new-style registry) H(TEXT("refreshAllNodes"), &FBlueprintMCPServer::HandleRefreshAllNodes); // 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); + // get_node_comment is now handled by UMCPHandler_GetNodeComment (new-style registry) + // set_node_comment is now handled by UMCPHandler_SetNodeComment (new-style registry) H(TEXT("getPinInfo"), &FBlueprintMCPServer::HandleGetPinInfo); H(TEXT("checkPinCompatibility"), &FBlueprintMCPServer::HandleCheckPinCompatibility); H(TEXT("listClasses"), &FBlueprintMCPServer::HandleListClasses); H(TEXT("listFunctions"), &FBlueprintMCPServer::HandleListFunctions); H(TEXT("listProperties"), &FBlueprintMCPServer::HandleListProperties); H(TEXT("changeStructNodeType"), &FBlueprintMCPServer::HandleChangeStructNodeType); - H(TEXT("deleteNode"), &FBlueprintMCPServer::HandleDeleteNode); + // delete_node is now handled by UMCPHandler_DeleteNode (new-style registry) // duplicate_nodes is now handled by UMCPHandler_DuplicateNodes (new-style registry) H(TEXT("validateBlueprint"), &FBlueprintMCPServer::HandleValidateBlueprint); H(TEXT("validateAllBlueprints"), &FBlueprintMCPServer::HandleValidateAllBlueprints); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintMCPHandlers_Mutation.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintMCPHandlers_Mutation.h index 8222e6f6..5e4386ce 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintMCPHandlers_Mutation.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintMCPHandlers_Mutation.h @@ -144,3 +144,144 @@ public: virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override; }; + +UCLASS(meta=(ToolName="get_node_comment")) +class UMCPHandler_GetNodeComment : public UMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Node GUID")) + FString NodeId; + + virtual FString GetDescription() const override + { + return TEXT("Get the comment text and bubble visibility of a node."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override; +}; + +UCLASS(meta=(ToolName="set_node_comment")) +class UMCPHandler_SetNodeComment : public UMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Node GUID")) + FString NodeId; + + UPROPERTY(meta=(Description="Comment text to set")) + FString Comment; + + virtual FString GetDescription() const override + { + return TEXT("Set a node's comment text. Makes the comment bubble visible if non-empty."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override; +}; + +UCLASS(meta=(ToolName="delete_node")) +class UMCPHandler_DeleteNode : public UMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Node GUID")) + FString NodeId; + + virtual FString GetDescription() const override + { + return TEXT("Delete a node from a Blueprint graph. " + "Cannot delete entry nodes (FunctionEntry, Event, CustomEvent)."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override; +}; + +USTRUCT() +struct FConnectPinsEntry +{ + GENERATED_BODY() + + UPROPERTY() + FString SourceNodeId; + + UPROPERTY() + FString SourcePinName; + + UPROPERTY() + FString TargetNodeId; + + UPROPERTY() + FString TargetPinName; +}; + +UCLASS(meta=(ToolName="connect_pins")) +class UMCPHandler_ConnectPins : public UMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Array of {sourceNodeId, sourcePinName, targetNodeId, targetPinName} objects")) + FMCPJsonArray Connections; + + virtual FString GetDescription() const override + { + return TEXT("Connect pins between nodes in a Blueprint graph."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override; +}; + +USTRUCT() +struct FDisconnectPinEntry +{ + GENERATED_BODY() + + UPROPERTY() + FString NodeId; + + UPROPERTY() + FString PinName; + + UPROPERTY(meta=(Optional)) + FString TargetNodeId; + + UPROPERTY(meta=(Optional)) + FString TargetPinName; +}; + +UCLASS(meta=(ToolName="disconnect_pin")) +class UMCPHandler_DisconnectPin : public UMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Array of {nodeId, pinName, targetNodeId?, targetPinName?} objects. If target is omitted, all connections on the pin are broken.")) + FMCPJsonArray Disconnections; + + virtual FString GetDescription() const override + { + return TEXT("Disconnect pins in a Blueprint graph. " + "Can disconnect a specific link or all links on a pin."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override; +}; diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintMCPServer.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintMCPServer.h index 1a267032..1bc9cba9 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintMCPServer.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintMCPServer.h @@ -141,7 +141,6 @@ private: void HandleChangeFunctionParamType(const FJsonObject* Json, FJsonObject* Result); void HandleRemoveFunctionParameter(const FJsonObject* Json, FJsonObject* Result); void HandleDeleteAsset(const FJsonObject* Json, FJsonObject* Result); - void HandleDeleteNode(const FJsonObject* Json, FJsonObject* Result); void HandleAddNode(const FJsonObject* Json, FJsonObject* Result); void HandleRenameAsset(const FJsonObject* Json, FJsonObject* Result); @@ -150,11 +149,7 @@ private: void HandleValidateAllBlueprints(const FJsonObject* Json, FJsonObject* Result); // ----- Pin manipulation (write) ----- - void HandleConnectPins(const FJsonObject* Json, FJsonObject* Result); - void HandleDisconnectPin(const FJsonObject* Json, FJsonObject* Result); void HandleRefreshAllNodes(const FJsonObject* Json, FJsonObject* Result); - void HandleGetNodeComment(const FJsonObject* Json, FJsonObject* Result); - void HandleSetNodeComment(const FJsonObject* Json, FJsonObject* Result); // ----- Pin introspection (read-only) ----- void HandleGetPinInfo(const FJsonObject* Json, FJsonObject* Result);