Another batch of MCP handlers ported

This commit is contained in:
2026-03-06 03:14:14 -05:00
parent 282548e2f3
commit 7351756a71
4 changed files with 341 additions and 241 deletions

View File

@@ -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<TSharedPtr<FJsonValue>> Results;
int32 SuccessCount = 0;
// Find target node
UEdGraphNode* TargetNode = FindNodeByGuid(BP, TargetNodeId);
if (!TargetNode)
for (const TSharedPtr<FJsonValue>& ConnVal : Connections.Array)
{
return MakeErrorJson(Result, FString::Printf(TEXT("Target node '%s' not found"), *TargetNodeId));
}
TSharedRef<FJsonObject> EntryResult = MakeShared<FJsonObject>();
Results.Add(MakeShared<FJsonValueObject>(EntryResult));
// Find source pin
UEdGraphPin* SourcePin = SourceNode->FindPin(FName(*SourcePinName));
if (!SourcePin)
{
// List available pins for debugging
TArray<TSharedPtr<FJsonValue>> PinNames;
for (UEdGraphPin* P : SourceNode->Pins)
FConnectPinsEntry Entry;
FString PopulateError = Helper->PopulateFromJson(FConnectPinsEntry::StaticStruct(), &Entry, ConnVal);
if (!PopulateError.IsEmpty())
{
if (P) PinNames.Add(MakeShared<FJsonValueString>(
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<TSharedPtr<FJsonValue>> 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<FJsonValueString>(
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<FJsonObject> SourceNodeState = SerializeNode(SourceNode);
TSharedPtr<FJsonObject> 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<TSharedPtr<FJsonValue>> Results;
int32 SuccessCount = 0;
int32 TotalDisconnected = 0;
// Find pin
UEdGraphPin* Pin = Node->FindPin(FName(*PinName));
if (!Pin)
for (const TSharedPtr<FJsonValue>& DiscVal : Disconnections.Array)
{
return MakeErrorJson(Result, FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *PinName, *NodeId));
}
TSharedRef<FJsonObject> EntryResult = MakeShared<FJsonObject>();
Results.Add(MakeShared<FJsonValueObject>(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<UK2Node_FunctionEntry>(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<UK2Node_Event>(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<UK2Node_CustomEvent>(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);
}
// ============================================================

View File

@@ -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);

View File

@@ -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;
};

View File

@@ -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);