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 // 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")); MCPHelper* Helper = MCPHelper::Get();
FString SourceNodeId = Json->GetStringField(TEXT("sourceNodeId"));
FString SourcePinName = Json->GetStringField(TEXT("sourcePinName"));
FString TargetNodeId = Json->GetStringField(TEXT("targetNodeId"));
FString TargetPinName = Json->GetStringField(TEXT("targetPinName"));
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; FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return Helper->MakeErrorJson(Result, LoadError);
} }
// Find source node TArray<TSharedPtr<FJsonValue>> Results;
UEdGraph* SourceGraph = nullptr; int32 SuccessCount = 0;
UEdGraphNode* SourceNode = FindNodeByGuid(BP, SourceNodeId, &SourceGraph);
if (!SourceNode)
{
return MakeErrorJson(Result, FString::Printf(TEXT("Source node '%s' not found"), *SourceNodeId));
}
// Find target node for (const TSharedPtr<FJsonValue>& ConnVal : Connections.Array)
UEdGraphNode* TargetNode = FindNodeByGuid(BP, TargetNodeId);
if (!TargetNode)
{ {
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 FConnectPinsEntry Entry;
UEdGraphPin* SourcePin = SourceNode->FindPin(FName(*SourcePinName)); FString PopulateError = Helper->PopulateFromJson(FConnectPinsEntry::StaticStruct(), &Entry, ConnVal);
if (!SourcePin) if (!PopulateError.IsEmpty())
{
// List available pins for debugging
TArray<TSharedPtr<FJsonValue>> PinNames;
for (UEdGraphPin* P : SourceNode->Pins)
{ {
if (P) PinNames.Add(MakeShared<FJsonValueString>( EntryResult->SetStringField(TEXT("error"), PopulateError);
FString::Printf(TEXT("%s (%s)"), *P->PinName.ToString(), continue;
P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output"))));
} }
MakeErrorJson(Result, FString::Printf(TEXT("Source pin '%s' not found on node '%s'"),
*SourcePinName, *SourceNodeId));
Result->SetArrayField(TEXT("availablePins"), PinNames);
return;
}
// Find target pin EntryResult->SetStringField(TEXT("sourceNodeId"), Entry.SourceNodeId);
UEdGraphPin* TargetPin = TargetNode->FindPin(FName(*TargetPinName)); EntryResult->SetStringField(TEXT("sourcePinName"), Entry.SourcePinName);
if (!TargetPin) EntryResult->SetStringField(TEXT("targetNodeId"), Entry.TargetNodeId);
{ EntryResult->SetStringField(TEXT("targetPinName"), Entry.TargetPinName);
// List available pins for debugging
TArray<TSharedPtr<FJsonValue>> PinNames; UEdGraph* SourceGraph = nullptr;
for (UEdGraphPin* P : TargetNode->Pins) UEdGraphNode* SourceNode = Helper->FindNodeByGuid(BP, Entry.SourceNodeId, &SourceGraph);
if (!SourceNode)
{ {
if (P) PinNames.Add(MakeShared<FJsonValueString>( EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Source node '%s' not found"), *Entry.SourceNodeId));
FString::Printf(TEXT("%s (%s)"), *P->PinName.ToString(), continue;
P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output"))));
} }
MakeErrorJson(Result, FString::Printf(TEXT("Target pin '%s' not found on node '%s'"),
*TargetPinName, *TargetNodeId)); UEdGraphNode* TargetNode = Helper->FindNodeByGuid(BP, Entry.TargetNodeId);
Result->SetArrayField(TEXT("availablePins"), PinNames); if (!TargetNode)
return; {
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"), if (SuccessCount > 0)
*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)
{ {
return MakeErrorJson(Result, TEXT("Graph schema not found")); FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
}
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);
} }
// Save UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: ConnectPins — %d/%d succeeded in '%s'"),
bool bSaved = SaveBlueprintPackage(BP); SuccessCount, Connections.Array.Num(), *Blueprint);
Result->SetBoolField(TEXT("saved"), bSaved);
// Return updated node state for both source and target Result->SetBoolField(TEXT("success"), true);
TSharedPtr<FJsonObject> SourceNodeState = SerializeNode(SourceNode); Result->SetStringField(TEXT("blueprint"), Blueprint);
TSharedPtr<FJsonObject> TargetNodeState = SerializeNode(TargetNode); Result->SetNumberField(TEXT("successCount"), SuccessCount);
if (SourceNodeState.IsValid()) Result->SetNumberField(TEXT("totalCount"), Connections.Array.Num());
Result->SetObjectField(TEXT("updatedSourceNode"), SourceNodeState); Result->SetArrayField(TEXT("results"), Results);
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"));
} }
// ============================================================ // ============================================================
// HandleDisconnectPin — break connections on a pin // 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")); MCPHelper* Helper = MCPHelper::Get();
FString NodeId = Json->GetStringField(TEXT("nodeId"));
FString PinName = Json->GetStringField(TEXT("pinName"));
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; FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return Helper->MakeErrorJson(Result, LoadError);
} }
// Find source node TArray<TSharedPtr<FJsonValue>> Results;
UEdGraphNode* Node = FindNodeByGuid(BP, NodeId); int32 SuccessCount = 0;
if (!Node) int32 TotalDisconnected = 0;
{
return MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *NodeId));
}
// Find pin for (const TSharedPtr<FJsonValue>& DiscVal : Disconnections.Array)
UEdGraphPin* Pin = Node->FindPin(FName(*PinName));
if (!Pin)
{ {
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; FDisconnectPinEntry Entry;
FString PopulateError = Helper->PopulateFromJson(FDisconnectPinEntry::StaticStruct(), &Entry, DiscVal);
if (!TargetNodeId.IsEmpty() && !TargetPinName.IsEmpty()) if (!PopulateError.IsEmpty())
{
// Disconnect a single specific link
UEdGraphNode* TargetNode = FindNodeByGuid(BP, TargetNodeId);
if (!TargetNode)
{ {
return MakeErrorJson(Result, FString::Printf(TEXT("Target node '%s' not found"), *TargetNodeId)); EntryResult->SetStringField(TEXT("error"), PopulateError);
continue;
} }
UEdGraphPin* TargetPin = TargetNode->FindPin(FName(*TargetPinName)); EntryResult->SetStringField(TEXT("nodeId"), Entry.NodeId);
if (!TargetPin) 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'"), EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.NodeId));
*TargetPinName, *TargetNodeId)); 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); Pin->BreakLinkTo(TargetPin);
DisconnectedCount = 1; DisconnectedCount = 1;
} }
else else
{ {
return MakeErrorJson(Result, TEXT("The specified pins are not connected to each other")); DisconnectedCount = Pin->LinkedTo.Num();
} if (DisconnectedCount > 0)
} {
else Pin->BreakAllPinLinks(true);
{ }
// Disconnect all links on this pin
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"), if (TotalDisconnected > 0)
DisconnectedCount, *Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString(), *PinName);
// Save
bool bSaved = false;
if (DisconnectedCount > 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->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("blueprint"), BlueprintName); Result->SetStringField(TEXT("blueprint"), Blueprint);
Result->SetNumberField(TEXT("disconnectedCount"), DisconnectedCount); Result->SetNumberField(TEXT("successCount"), SuccessCount);
Result->SetBoolField(TEXT("saved"), bSaved); 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 // 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")); MCPHelper* Helper = MCPHelper::Get();
FString NodeId = Json->GetStringField(TEXT("nodeId"));
if (BlueprintName.IsEmpty() || NodeId.IsEmpty())
{
return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, nodeId"));
}
// Load Blueprint
FString LoadError; FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return Helper->MakeErrorJson(Result, LoadError);
} }
// Find node
UEdGraph* Graph = nullptr; UEdGraph* Graph = nullptr;
UEdGraphNode* Node = FindNodeByGuid(BP, NodeId, &Graph); UEdGraphNode* Node = Helper->FindNodeByGuid(BP, NodeId, &Graph);
if (!Node) 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) 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(); FString NodeClass = Node->GetClass()->GetName();
@@ -1167,7 +1154,7 @@ void FBlueprintMCPServer::HandleDeleteNode(const FJsonObject* Json, FJsonObject*
// without recreating the entire function/event. // without recreating the entire function/event.
if (Cast<UK2Node_FunctionEntry>(Node)) 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("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("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."), 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)) 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("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."), TEXT("This is the root node of the event handler — removing it would leave an empty, uncompilable graph."),
*NodeTitle, *GraphName)); *NodeTitle, *GraphName));
} }
if (Cast<UK2Node_CustomEvent>(Node)) 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("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."), TEXT("This is the root node of the custom event — removing it would leave an empty, uncompilable graph."),
*NodeTitle, *GraphName)); *NodeTitle, *GraphName));
} }
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleting node '%s' (%s) from graph '%s' in '%s'"), 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(); Node->BreakAllNodeLinks();
// Remove the node from the graph
Graph->RemoveNode(Node); Graph->RemoveNode(Node);
// Save (which also compiles) FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
bool bSaved = SaveBlueprintPackage(BP);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Node deleted, save %s"), UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Node deleted"));
bSaved ? TEXT("succeeded") : TEXT("failed"));
Result->SetBoolField(TEXT("success"), true); Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("blueprint"), BlueprintName); Result->SetStringField(TEXT("blueprint"), Blueprint);
Result->SetStringField(TEXT("nodeId"), NodeId); Result->SetStringField(TEXT("nodeId"), NodeId);
Result->SetStringField(TEXT("nodeClass"), NodeClass); Result->SetStringField(TEXT("nodeClass"), NodeClass);
Result->SetStringField(TEXT("nodeTitle"), NodeTitle); Result->SetStringField(TEXT("nodeTitle"), NodeTitle);
Result->SetStringField(TEXT("graph"), GraphName); 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 // HandleGetNodeComment — read a node's comment text
// ============================================================ // ============================================================
void FBlueprintMCPServer::HandleGetNodeComment(const FJsonObject* Json, FJsonObject* Result) // get_node_comment is now handled by UMCPHandler_GetNodeComment (new-style registry)
{
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
FString NodeId = Json->GetStringField(TEXT("nodeId"));
if (BlueprintName.IsEmpty() || NodeId.IsEmpty()) void UMCPHandler_GetNodeComment::Handle(const FJsonObject* Json, FJsonObject* Result)
{ {
return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, nodeId")); MCPHelper* Helper = MCPHelper::Get();
}
FString LoadError; FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return Helper->MakeErrorJson(Result, LoadError);
} }
UEdGraphNode* Node = FindNodeByGuid(BP, NodeId); UEdGraphNode* Node = Helper->FindNodeByGuid(BP, NodeId);
if (!Node) 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->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("blueprint"), BlueprintName); Result->SetStringField(TEXT("blueprint"), Blueprint);
Result->SetStringField(TEXT("nodeId"), NodeId); Result->SetStringField(TEXT("nodeId"), NodeId);
Result->SetStringField(TEXT("comment"), Node->NodeComment); Result->SetStringField(TEXT("comment"), Node->NodeComment);
Result->SetBoolField(TEXT("commentBubbleVisible"), Node->bCommentBubbleVisible); Result->SetBoolField(TEXT("commentBubbleVisible"), Node->bCommentBubbleVisible);
@@ -2255,34 +2232,23 @@ void FBlueprintMCPServer::HandleGetNodeComment(const FJsonObject* Json, FJsonObj
// HandleSetNodeComment — set a node's comment text // 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")); MCPHelper* Helper = MCPHelper::Get();
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"));
FString LoadError; FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError);
if (!BP) if (!BP)
{ {
return MakeErrorJson(Result, LoadError); return Helper->MakeErrorJson(Result, LoadError);
} }
UEdGraphNode* Node = FindNodeByGuid(BP, NodeId); UEdGraphNode* Node = Helper->FindNodeByGuid(BP, NodeId);
if (!Node) 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; FString OldComment = Node->NodeComment;
@@ -2296,17 +2262,15 @@ void FBlueprintMCPServer::HandleSetNodeComment(const FJsonObject* Json, FJsonObj
} }
FBlueprintEditorUtils::MarkBlueprintAsModified(BP); FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
bool bSaved = SaveBlueprintPackage(BP);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Set comment on node '%s' in '%s', save %s"), UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Set comment on node '%s' in '%s'"),
*NodeId, *BlueprintName, bSaved ? TEXT("succeeded") : TEXT("failed")); *NodeId, *Blueprint);
Result->SetBoolField(TEXT("success"), true); Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("blueprint"), BlueprintName); Result->SetStringField(TEXT("blueprint"), Blueprint);
Result->SetStringField(TEXT("nodeId"), NodeId); Result->SetStringField(TEXT("nodeId"), NodeId);
Result->SetStringField(TEXT("oldComment"), OldComment); Result->SetStringField(TEXT("oldComment"), OldComment);
Result->SetStringField(TEXT("newComment"), Comment); 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, Router->BindRoute(FHttpPath(TEXT("/api/test-save")), EHttpServerRequestVerbs::VERB_GET,
QueuedHandler(TEXT("testSave"))); QueuedHandler(TEXT("testSave")));
Router->BindRoute(FHttpPath(TEXT("/api/connect-pins")), EHttpServerRequestVerbs::VERB_POST, 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, 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, Router->BindRoute(FHttpPath(TEXT("/api/refresh-all-nodes")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("refreshAllNodes"))); QueuedHandler(TEXT("refreshAllNodes")));
Router->BindRoute(FHttpPath(TEXT("/api/set-pin-default")), EHttpServerRequestVerbs::VERB_POST, Router->BindRoute(FHttpPath(TEXT("/api/set-pin-default")), EHttpServerRequestVerbs::VERB_POST,
@@ -621,9 +621,9 @@ bool FBlueprintMCPServer::Start(int32 InPort, bool bEditorMode)
Router->BindRoute(FHttpPath(TEXT("/api/move-node")), EHttpServerRequestVerbs::VERB_POST, Router->BindRoute(FHttpPath(TEXT("/api/move-node")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("move_node"))); QueuedHandler(TEXT("move_node")));
Router->BindRoute(FHttpPath(TEXT("/api/get-node-comment")), EHttpServerRequestVerbs::VERB_POST, Router->BindRoute(FHttpPath(TEXT("/api/get-node-comment")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("getNodeComment"))); QueuedHandler(TEXT("get_node_comment")));
Router->BindRoute(FHttpPath(TEXT("/api/set-node-comment")), EHttpServerRequestVerbs::VERB_POST, 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, Router->BindRoute(FHttpPath(TEXT("/api/get-pin-info")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("getPinInfo"))); QueuedHandler(TEXT("getPinInfo")));
Router->BindRoute(FHttpPath(TEXT("/api/check-pin-compatibility")), EHttpServerRequestVerbs::VERB_POST, 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, Router->BindRoute(FHttpPath(TEXT("/api/remove-function-parameter")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("removeFunctionParameter"))); QueuedHandler(TEXT("removeFunctionParameter")));
Router->BindRoute(FHttpPath(TEXT("/api/delete-node")), EHttpServerRequestVerbs::VERB_POST, Router->BindRoute(FHttpPath(TEXT("/api/delete-node")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("deleteNode"))); QueuedHandler(TEXT("delete_node")));
Router->BindRoute(FHttpPath(TEXT("/api/duplicate-nodes")), EHttpServerRequestVerbs::VERB_POST, Router->BindRoute(FHttpPath(TEXT("/api/duplicate-nodes")), EHttpServerRequestVerbs::VERB_POST,
QueuedHandler(TEXT("duplicate_nodes"))); QueuedHandler(TEXT("duplicate_nodes")));
Router->BindRoute(FHttpPath(TEXT("/api/search-by-type")), EHttpServerRequestVerbs::VERB_GET, Router->BindRoute(FHttpPath(TEXT("/api/search-by-type")), EHttpServerRequestVerbs::VERB_GET,
@@ -982,17 +982,17 @@ void FBlueprintMCPServer::RegisterHandlers()
TEXT("changeFunctionParamType"), TEXT("changeFunctionParamType"),
TEXT("removeFunctionParameter"), TEXT("removeFunctionParameter"),
TEXT("deleteAsset"), TEXT("deleteAsset"),
TEXT("connectPins"), TEXT("connect_pins"),
TEXT("disconnectPin"), TEXT("disconnect_pin"),
TEXT("refreshAllNodes"), TEXT("refreshAllNodes"),
TEXT("set_pin_default"), TEXT("set_pin_default"),
TEXT("move_node"), TEXT("move_node"),
TEXT("changeStructNodeType"), TEXT("changeStructNodeType"),
TEXT("deleteNode"), TEXT("delete_node"),
TEXT("duplicate_nodes"), TEXT("duplicate_nodes"),
TEXT("addNode"), TEXT("addNode"),
TEXT("spawn_node"), TEXT("spawn_node"),
TEXT("setNodeComment"), TEXT("set_node_comment"),
TEXT("renameAsset"), TEXT("renameAsset"),
TEXT("reparentBlueprint"), TEXT("reparentBlueprint"),
TEXT("setBlueprintDefault"), TEXT("setBlueprintDefault"),
@@ -1055,20 +1055,20 @@ void FBlueprintMCPServer::RegisterHandlers()
H(TEXT("changeFunctionParamType"), &FBlueprintMCPServer::HandleChangeFunctionParamType); H(TEXT("changeFunctionParamType"), &FBlueprintMCPServer::HandleChangeFunctionParamType);
H(TEXT("removeFunctionParameter"), &FBlueprintMCPServer::HandleRemoveFunctionParameter); H(TEXT("removeFunctionParameter"), &FBlueprintMCPServer::HandleRemoveFunctionParameter);
H(TEXT("deleteAsset"), &FBlueprintMCPServer::HandleDeleteAsset); H(TEXT("deleteAsset"), &FBlueprintMCPServer::HandleDeleteAsset);
H(TEXT("connectPins"), &FBlueprintMCPServer::HandleConnectPins); // connect_pins is now handled by UMCPHandler_ConnectPins (new-style registry)
H(TEXT("disconnectPin"), &FBlueprintMCPServer::HandleDisconnectPin); // disconnect_pin is now handled by UMCPHandler_DisconnectPin (new-style registry)
H(TEXT("refreshAllNodes"), &FBlueprintMCPServer::HandleRefreshAllNodes); H(TEXT("refreshAllNodes"), &FBlueprintMCPServer::HandleRefreshAllNodes);
// set_pin_default is now handled by UMCPHandler_SetPinDefault (new-style registry) // set_pin_default is now handled by UMCPHandler_SetPinDefault (new-style registry)
// move_node is now handled by UMCPHandler_MoveNode (new-style registry) // move_node is now handled by UMCPHandler_MoveNode (new-style registry)
H(TEXT("getNodeComment"), &FBlueprintMCPServer::HandleGetNodeComment); // get_node_comment is now handled by UMCPHandler_GetNodeComment (new-style registry)
H(TEXT("setNodeComment"), &FBlueprintMCPServer::HandleSetNodeComment); // set_node_comment is now handled by UMCPHandler_SetNodeComment (new-style registry)
H(TEXT("getPinInfo"), &FBlueprintMCPServer::HandleGetPinInfo); H(TEXT("getPinInfo"), &FBlueprintMCPServer::HandleGetPinInfo);
H(TEXT("checkPinCompatibility"), &FBlueprintMCPServer::HandleCheckPinCompatibility); H(TEXT("checkPinCompatibility"), &FBlueprintMCPServer::HandleCheckPinCompatibility);
H(TEXT("listClasses"), &FBlueprintMCPServer::HandleListClasses); H(TEXT("listClasses"), &FBlueprintMCPServer::HandleListClasses);
H(TEXT("listFunctions"), &FBlueprintMCPServer::HandleListFunctions); H(TEXT("listFunctions"), &FBlueprintMCPServer::HandleListFunctions);
H(TEXT("listProperties"), &FBlueprintMCPServer::HandleListProperties); H(TEXT("listProperties"), &FBlueprintMCPServer::HandleListProperties);
H(TEXT("changeStructNodeType"), &FBlueprintMCPServer::HandleChangeStructNodeType); H(TEXT("changeStructNodeType"), &FBlueprintMCPServer::HandleChangeStructNodeType);
H(TEXT("deleteNode"), &FBlueprintMCPServer::HandleDeleteNode); // delete_node is now handled by UMCPHandler_DeleteNode (new-style registry)
// duplicate_nodes is now handled by UMCPHandler_DuplicateNodes (new-style registry) // duplicate_nodes is now handled by UMCPHandler_DuplicateNodes (new-style registry)
H(TEXT("validateBlueprint"), &FBlueprintMCPServer::HandleValidateBlueprint); H(TEXT("validateBlueprint"), &FBlueprintMCPServer::HandleValidateBlueprint);
H(TEXT("validateAllBlueprints"), &FBlueprintMCPServer::HandleValidateAllBlueprints); H(TEXT("validateAllBlueprints"), &FBlueprintMCPServer::HandleValidateAllBlueprints);

View File

@@ -144,3 +144,144 @@ public:
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override; 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 HandleChangeFunctionParamType(const FJsonObject* Json, FJsonObject* Result);
void HandleRemoveFunctionParameter(const FJsonObject* Json, FJsonObject* Result); void HandleRemoveFunctionParameter(const FJsonObject* Json, FJsonObject* Result);
void HandleDeleteAsset(const FJsonObject* Json, FJsonObject* Result); void HandleDeleteAsset(const FJsonObject* Json, FJsonObject* Result);
void HandleDeleteNode(const FJsonObject* Json, FJsonObject* Result);
void HandleAddNode(const FJsonObject* Json, FJsonObject* Result); void HandleAddNode(const FJsonObject* Json, FJsonObject* Result);
void HandleRenameAsset(const FJsonObject* Json, FJsonObject* Result); void HandleRenameAsset(const FJsonObject* Json, FJsonObject* Result);
@@ -150,11 +149,7 @@ private:
void HandleValidateAllBlueprints(const FJsonObject* Json, FJsonObject* Result); void HandleValidateAllBlueprints(const FJsonObject* Json, FJsonObject* Result);
// ----- Pin manipulation (write) ----- // ----- 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 HandleRefreshAllNodes(const FJsonObject* Json, FJsonObject* Result);
void HandleGetNodeComment(const FJsonObject* Json, FJsonObject* Result);
void HandleSetNodeComment(const FJsonObject* Json, FJsonObject* Result);
// ----- Pin introspection (read-only) ----- // ----- Pin introspection (read-only) -----
void HandleGetPinInfo(const FJsonObject* Json, FJsonObject* Result); void HandleGetPinInfo(const FJsonObject* Json, FJsonObject* Result);