From 3ed7a33f6983e68604a504b824eff427f2a37ed2 Mon Sep 17 00:00:00 2001 From: jyelon Date: Fri, 6 Mar 2026 04:18:16 -0500 Subject: [PATCH] More work on MCP endpoints --- Content/Widgets/WB_Hotkeys.uasset | 4 +- .../Private/BlueprintMCPHandlers_Mutation.cpp | 890 +++--------------- .../Private/BlueprintMCPServer.cpp | 63 +- .../Public/BlueprintMCPHandlers_Mutation.h | 155 +++ .../BlueprintMCP/Public/BlueprintMCPServer.h | 28 +- 5 files changed, 313 insertions(+), 827 deletions(-) diff --git a/Content/Widgets/WB_Hotkeys.uasset b/Content/Widgets/WB_Hotkeys.uasset index ef5f8336..8865beeb 100644 --- a/Content/Widgets/WB_Hotkeys.uasset +++ b/Content/Widgets/WB_Hotkeys.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:43c0640d859e140bd0efeabcc932eb680f02203657da891981a3705acd9da499 -size 261317 +oid sha256:27eced69df36e89b5136a4ce2a32de506296e05d5194af0bbc051064a59aaa6f +size 253523 diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Mutation.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Mutation.cpp index 1aac2c26..826ad1c0 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Mutation.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_Mutation.cpp @@ -45,79 +45,60 @@ #include "BlueprintActionDatabase.h" #include "BlueprintNodeSpawner.h" -// SEH wrapper defined in BlueprintMCPServer.cpp — non-static for cross-TU access -#if PLATFORM_WINDOWS -extern int32 TryRefreshAllNodesSEH(UBlueprint* BP); -#endif // ============================================================ -// HandleReplaceFunctionCalls — redirect function call nodes +// ReplaceFunctionCalls — redirect function call nodes // ============================================================ -void FBlueprintMCPServer::HandleReplaceFunctionCalls(const FJsonObject* Json, FJsonObject* Result) +void UMCPHandler_ReplaceFunctionCalls::Handle(const FJsonObject* Json, FJsonObject* Result) { - FString BlueprintName = Json->GetStringField(TEXT("blueprint")); - FString OldClassName = Json->GetStringField(TEXT("oldClass")); - FString NewClassName = Json->GetStringField(TEXT("newClass")); - - if (BlueprintName.IsEmpty() || OldClassName.IsEmpty() || NewClassName.IsEmpty()) - { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, oldClass, newClass")); - } + MCPHelper* Helper = MCPHelper::Get(); // Load Blueprint FString LoadError; - UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); + UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return Helper->MakeErrorJson(Result, LoadError); } // Find the new class — try several search strategies - UClass* NewClass = nullptr; + UClass* NewClassPtr = nullptr; // Try finding the class across all loaded modules - NewClass = FindFirstObject(*NewClassName); + NewClassPtr = FindFirstObject(*NewClass); // Try with U prefix stripped/added - if (!NewClass && NewClassName.StartsWith(TEXT("U"))) + if (!NewClassPtr && NewClass.StartsWith(TEXT("U"))) { - FString WithoutU = NewClassName.Mid(1); - NewClass = FindFirstObject(*WithoutU); + NewClassPtr = FindFirstObject(*NewClass.Mid(1)); } - if (!NewClass && !NewClassName.StartsWith(TEXT("U"))) + if (!NewClassPtr && !NewClass.StartsWith(TEXT("U"))) { - NewClass = FindFirstObject(*FString::Printf(TEXT("U%s"), *NewClassName)); + NewClassPtr = FindFirstObject(*FString::Printf(TEXT("U%s"), *NewClass)); } // Broader search across all modules - if (!NewClass) + if (!NewClassPtr) { for (TObjectIterator It; It; ++It) { - if (It->GetName() == NewClassName || It->GetName() == FString::Printf(TEXT("U%s"), *NewClassName)) + if (It->GetName() == NewClass || It->GetName() == FString::Printf(TEXT("U%s"), *NewClass)) { - NewClass = *It; + NewClassPtr = *It; break; } } } - if (!NewClass) + if (!NewClassPtr) { - return MakeErrorJson(Result, FString::Printf(TEXT("Could not find class '%s'"), *NewClassName)); - } - - // Check for dry run - bool bDryRun = false; - if (Json->HasField(TEXT("dryRun"))) - { - bDryRun = Json->GetBoolField(TEXT("dryRun")); + return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Could not find class '%s'"), *NewClass)); } UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s function calls in '%s': %s -> %s (%s)"), - bDryRun ? TEXT("[DRY RUN] Analyzing replacement of") : TEXT("Replacing"), - *BlueprintName, *OldClassName, *NewClassName, *NewClass->GetPathName()); + DryRun ? TEXT("[DRY RUN] Analyzing replacement of") : TEXT("Replacing"), + *Blueprint, *OldClass, *NewClass, *NewClassPtr->GetPathName()); // Find all CallFunction nodes TArray AllCallNodes; @@ -136,11 +117,11 @@ void FBlueprintMCPServer::HandleReplaceFunctionCalls(const FJsonObject* Json, FJ // Match by class name (with or without U prefix, and _C suffix for BP classes) FString ParentName = ParentClass->GetName(); - bool bMatch = ParentName == OldClassName || - ParentName == FString::Printf(TEXT("%s_C"), *OldClassName) || - ParentName == FString::Printf(TEXT("U%s"), *OldClassName) || - (OldClassName.StartsWith(TEXT("U")) && ParentName == OldClassName.Mid(1)) || - (OldClassName.EndsWith(TEXT("_C")) && ParentName == OldClassName.LeftChop(2)); + bool bMatch = (ParentName == OldClass) || + (ParentName == FString::Printf(TEXT("%s_C"), *OldClass)) || + (ParentName == FString::Printf(TEXT("U%s"), *OldClass)) || + (OldClass.StartsWith(TEXT("U")) && (ParentName == OldClass.Mid(1))) || + (OldClass.EndsWith(TEXT("_C")) && (ParentName == OldClass.LeftChop(2))); if (!bMatch) { @@ -150,11 +131,11 @@ void FBlueprintMCPServer::HandleReplaceFunctionCalls(const FJsonObject* Json, FJ FName FuncName = CallNode->FunctionReference.GetMemberName(); // Find the matching function in the new class - UFunction* NewFunc = NewClass->FindFunctionByName(FuncName); + UFunction* NewFunc = NewClassPtr->FindFunctionByName(FuncName); if (!NewFunc) { UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Function '%s' not found in '%s', skipping node"), - *FuncName.ToString(), *NewClassName); + *FuncName.ToString(), *NewClass); TSharedRef Warning = MakeShared(); Warning->SetStringField(TEXT("type"), TEXT("functionNotFound")); @@ -164,7 +145,7 @@ void FBlueprintMCPServer::HandleReplaceFunctionCalls(const FJsonObject* Json, FJ continue; } - if (bDryRun) + if (DryRun) { // In dry run mode: report what would be affected without modifying ReplacedCount++; @@ -273,54 +254,42 @@ void FBlueprintMCPServer::HandleReplaceFunctionCalls(const FJsonObject* Json, FJ } } - if (bDryRun) + if (DryRun) { Result->SetBoolField(TEXT("dryRun"), true); - Result->SetStringField(TEXT("blueprint"), BlueprintName); + Result->SetStringField(TEXT("blueprint"), Blueprint); Result->SetNumberField(TEXT("wouldReplaceCount"), ReplacedCount); Result->SetNumberField(TEXT("connectionsAtRisk"), BrokenConnections.Num()); Result->SetArrayField(TEXT("connectionsAtRisk"), BrokenConnections); return; } - // Save — guard flags and SEH protection are handled inside SaveBlueprintPackage if (ReplacedCount > 0) { - bool bSaved = SaveBlueprintPackage(BP); - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Replaced %d function call(s), save %s"), - ReplacedCount, bSaved ? TEXT("succeeded") : TEXT("failed")); + FBlueprintEditorUtils::MarkBlueprintAsModified(BP); - Result->SetStringField(TEXT("blueprint"), BlueprintName); + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Replaced %d function call(s)"), ReplacedCount); + + Result->SetStringField(TEXT("blueprint"), Blueprint); Result->SetNumberField(TEXT("replacedCount"), ReplacedCount); - Result->SetBoolField(TEXT("saved"), bSaved); Result->SetNumberField(TEXT("brokenConnectionCount"), BrokenConnections.Num()); Result->SetArrayField(TEXT("brokenConnections"), BrokenConnections); return; } - Result->SetStringField(TEXT("blueprint"), BlueprintName); + Result->SetStringField(TEXT("blueprint"), Blueprint); Result->SetNumberField(TEXT("replacedCount"), 0); Result->SetStringField(TEXT("message"), FString::Printf( - TEXT("No function call nodes found targeting class '%s'"), *OldClassName)); + TEXT("No function call nodes found targeting class '%s'"), *OldClass)); } // ============================================================ -// HandleDeleteAsset — delete a .uasset after verifying no references +// DeleteAsset — delete a .uasset after verifying no references // ============================================================ -void FBlueprintMCPServer::HandleDeleteAsset(const FJsonObject* Json, FJsonObject* Result) +void UMCPHandler_DeleteAsset::Handle(const FJsonObject* Json, FJsonObject* Result) { - FString AssetPath = Json->GetStringField(TEXT("assetPath")); - if (AssetPath.IsEmpty()) - { - return MakeErrorJson(Result, TEXT("Missing required field: assetPath")); - } - - bool bForce = false; - if (Json->HasField(TEXT("force"))) - { - bForce = Json->GetBoolField(TEXT("force")); - } + MCPHelper* Helper = MCPHelper::Get(); // Check if asset file exists on disk FString PackageFilename = FPackageName::LongPackageNameToFilename( @@ -329,7 +298,7 @@ void FBlueprintMCPServer::HandleDeleteAsset(const FJsonObject* Json, FJsonObject if (!IFileManager::Get().FileExists(*PackageFilename)) { - return MakeErrorJson(Result, FString::Printf(TEXT("Asset file not found on disk: %s"), *PackageFilename)); + return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Asset file not found on disk: %s"), *PackageFilename)); } // Check references @@ -338,11 +307,11 @@ void FBlueprintMCPServer::HandleDeleteAsset(const FJsonObject* Json, FJsonObject Registry.GetReferencers(FName(*AssetPath), Referencers); // Filter out self-references - Referencers.RemoveAll([&AssetPath](const FName& Ref) { + Referencers.RemoveAll([this](const FName& Ref) { return Ref.ToString() == AssetPath; }); - if (Referencers.Num() > 0 && !bForce) + if ((Referencers.Num() > 0) && !Force) { // Classify references as "live" (loaded in memory) vs "stale" (only on disk) TArray> LiveRefs; @@ -361,7 +330,7 @@ void FBlueprintMCPServer::HandleDeleteAsset(const FJsonObject* Json, FJsonObject } } - MakeErrorJson(Result, TEXT("Asset is still referenced. Remove all references first.")); + Helper->MakeErrorJson(Result, TEXT("Asset is still referenced. Remove all references first.")); Result->SetStringField(TEXT("assetPath"), AssetPath); Result->SetNumberField(TEXT("referencerCount"), Referencers.Num()); Result->SetNumberField(TEXT("liveReferencerCount"), LiveRefs.Num()); @@ -377,7 +346,7 @@ void FBlueprintMCPServer::HandleDeleteAsset(const FJsonObject* Json, FJsonObject // Force delete: unload the package from memory first TArray> RefWarnings; - if (bForce) + if (Force) { // Collect reference warnings when force-deleting with existing references for (const FName& Ref : Referencers) @@ -409,14 +378,13 @@ void FBlueprintMCPServer::HandleDeleteAsset(const FJsonObject* Json, FJsonObject // Reset loaders to release file handles ResetLoaders(Package); - // Force garbage collection to free the objects CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); } } UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleting asset '%s' (%s)%s"), - *AssetPath, *PackageFilename, bForce ? TEXT(" [FORCE]") : TEXT("")); + *AssetPath, *PackageFilename, Force ? TEXT(" [FORCE]") : TEXT("")); // Delete the file on disk bool bDeleted = IFileManager::Get().Delete(*PackageFilename, false, true); @@ -424,7 +392,7 @@ void FBlueprintMCPServer::HandleDeleteAsset(const FJsonObject* Json, FJsonObject if (bDeleted) { // Remove from our cached asset list if present - AllBlueprintAssets.RemoveAll([&AssetPath](const FAssetData& Asset) { + Helper->AllBlueprintAssets.RemoveAll([this](const FAssetData& Asset) { return Asset.PackageName.ToString() == AssetPath; }); @@ -444,10 +412,10 @@ void FBlueprintMCPServer::HandleDeleteAsset(const FJsonObject* Json, FJsonObject Result->SetBoolField(TEXT("success"), bDeleted); Result->SetStringField(TEXT("assetPath"), AssetPath); Result->SetStringField(TEXT("filename"), PackageFilename); - Result->SetBoolField(TEXT("forced"), bForce); + Result->SetBoolField(TEXT("forced"), Force); if (!bDeleted) { - MakeErrorJson(Result, TEXT("Failed to delete file from disk")); + Helper->MakeErrorJson(Result, TEXT("Failed to delete file from disk")); } if (RefWarnings.Num() > 0) { @@ -668,23 +636,19 @@ void UMCPHandler_DisconnectPin::Handle(const FJsonObject* Json, FJsonObject* Res } // ============================================================ -// HandleRefreshAllNodes — refresh all nodes and recompile +// RefreshAllNodes — refresh all nodes and recompile // ============================================================ -void FBlueprintMCPServer::HandleRefreshAllNodes(const FJsonObject* Json, FJsonObject* Result) +void UMCPHandler_RefreshAllNodes::Handle(const FJsonObject* Json, FJsonObject* Result) { - FString BlueprintName = Json->GetStringField(TEXT("blueprint")); - if (BlueprintName.IsEmpty()) - { - return MakeErrorJson(Result, TEXT("Missing required field: blueprint")); - } + MCPHelper* Helper = MCPHelper::Get(); // Load Blueprint FString LoadError; - UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); + UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return Helper->MakeErrorJson(Result, LoadError); } // Count graphs and nodes before refresh @@ -692,33 +656,23 @@ void FBlueprintMCPServer::HandleRefreshAllNodes(const FJsonObject* Json, FJsonOb BP->GetAllGraphs(AllGraphs); int32 GraphCount = AllGraphs.Num(); int32 NodeCount = 0; - for (UEdGraph* Graph : AllGraphs) + for (UEdGraph* G : AllGraphs) { - if (Graph) NodeCount += Graph->Nodes.Num(); + if (G) NodeCount += G->Nodes.Num(); } UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Refreshing all nodes in '%s' (%d graphs, %d nodes)"), - *BlueprintName, GraphCount, NodeCount); + *Blueprint, GraphCount, NodeCount); - // Refresh all nodes with SEH protection - bool bRefreshCrashed = false; -#if PLATFORM_WINDOWS - int32 RefreshResult = TryRefreshAllNodesSEH(BP); - if (RefreshResult != 0) - { - UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: RefreshAllNodes crashed (SEH), proceeding to save")); - bRefreshCrashed = true; - } -#else + // Refresh all nodes FBlueprintEditorUtils::RefreshAllNodes(BP); -#endif // Remove orphaned pins from all nodes int32 OrphanedPinsRemoved = 0; - for (UEdGraph* Graph : AllGraphs) + for (UEdGraph* G : AllGraphs) { - if (!Graph) continue; - for (UEdGraphNode* Node : Graph->Nodes) + if (!G) continue; + for (UEdGraphNode* Node : G->Nodes) { if (!Node) continue; for (int32 i = Node->Pins.Num() - 1; i >= 0; --i) @@ -737,15 +691,12 @@ void FBlueprintMCPServer::HandleRefreshAllNodes(const FJsonObject* Json, FJsonOb if (OrphanedPinsRemoved > 0) { UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed %d orphaned pins"), OrphanedPinsRemoved); - // Mark as modified and recompile after orphan removal - FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); } - // Save (which also compiles) - bool bSaved = SaveBlueprintPackage(BP); + // Mark as modified and recompile after orphan removal + FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: RefreshAllNodes complete, save %s"), - bSaved ? TEXT("succeeded") : TEXT("failed")); + UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: RefreshAllNodes complete")); // Collect compiler warnings and errors from the blueprint status TArray> WarningsArr; @@ -759,17 +710,17 @@ void FBlueprintMCPServer::HandleRefreshAllNodes(const FJsonObject* Json, FJsonOb // Check each graph for nodes with error/warning status AllGraphs.Empty(); BP->GetAllGraphs(AllGraphs); - for (UEdGraph* Graph : AllGraphs) + for (UEdGraph* G : AllGraphs) { - if (!Graph) continue; - for (UEdGraphNode* Node : Graph->Nodes) + if (!G) continue; + for (UEdGraphNode* Node : G->Nodes) { if (!Node) continue; if (Node->bHasCompilerMessage) { FString NodeTitle = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString(); FString NodeMsg = FString::Printf(TEXT("[%s] %s: %s"), - *Graph->GetName(), *NodeTitle, *Node->ErrorMsg); + *G->GetName(), *NodeTitle, *Node->ErrorMsg); if (Node->ErrorType == EMessageSeverity::Error) { ErrorsArr.Add(MakeShared(NodeMsg)); @@ -782,18 +733,13 @@ void FBlueprintMCPServer::HandleRefreshAllNodes(const FJsonObject* Json, FJsonOb } } - Result->SetBoolField(TEXT("success"), !bRefreshCrashed); - Result->SetStringField(TEXT("blueprint"), BlueprintName); + Result->SetBoolField(TEXT("success"), true); + Result->SetStringField(TEXT("blueprint"), Blueprint); Result->SetNumberField(TEXT("graphCount"), GraphCount); Result->SetNumberField(TEXT("nodeCount"), NodeCount); Result->SetNumberField(TEXT("orphanedPinsRemoved"), OrphanedPinsRemoved); - Result->SetBoolField(TEXT("saved"), bSaved); Result->SetArrayField(TEXT("warnings"), WarningsArr); Result->SetArrayField(TEXT("errors"), ErrorsArr); - if (bRefreshCrashed) - { - WarningsArr.Add(MakeShared(TEXT("RefreshAllNodes crashed (SEH caught), save was still attempted"))); - } } // ============================================================ @@ -898,34 +844,27 @@ void UMCPHandler_SetPinDefault::Handle(const FJsonObject* Json, FJsonObject* Res } // ============================================================ -// HandleChangeStructNodeType — change the struct type on a Break/Make node +// ChangeStructNodeType — change the struct type on a Break/Make node // ============================================================ -void FBlueprintMCPServer::HandleChangeStructNodeType(const FJsonObject* Json, FJsonObject* Result) +void UMCPHandler_ChangeStructNodeType::Handle(const FJsonObject* Json, FJsonObject* Result) { - FString BlueprintName = Json->GetStringField(TEXT("blueprint")); - FString NodeId = Json->GetStringField(TEXT("nodeId")); - FString NewType = Json->GetStringField(TEXT("newType")); - - if (BlueprintName.IsEmpty() || NodeId.IsEmpty() || NewType.IsEmpty()) - { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, nodeId, newType")); - } + MCPHelper* Helper = MCPHelper::Get(); // Load Blueprint FString LoadError; - UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); + UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return Helper->MakeErrorJson(Result, LoadError); } // 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)); } // Determine what kind of struct node this is @@ -934,7 +873,7 @@ void FBlueprintMCPServer::HandleChangeStructNodeType(const FJsonObject* Json, FJ if (!BreakNode && !MakeNode) { - return MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' is not a BreakStruct or MakeStruct node (class: %s)"), + return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' is not a BreakStruct or MakeStruct node (class: %s)"), *NodeId, *Node->GetClass()->GetName())); } @@ -953,7 +892,7 @@ void FBlueprintMCPServer::HandleChangeStructNodeType(const FJsonObject* Json, FJ } if (!NewStruct) { - return MakeErrorJson(Result, FString::Printf(TEXT("Struct type '%s' not found"), *NewType)); + return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Struct type '%s' not found"), *NewType)); } UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Changing struct node '%s' to type '%s'"), @@ -964,7 +903,7 @@ void FBlueprintMCPServer::HandleChangeStructNodeType(const FJsonObject* Json, FJ { // Find the last underscore before 32 hex chars (GUID) int32 LastUnderscore; - if (PinName.FindLastChar(TEXT('_'), LastUnderscore) && LastUnderscore > 0) + if (PinName.FindLastChar(TEXT('_'), LastUnderscore) && (LastUnderscore > 0)) { FString Suffix = PinName.Mid(LastUnderscore + 1); if (Suffix.Len() == 32) @@ -972,7 +911,7 @@ void FBlueprintMCPServer::HandleChangeStructNodeType(const FJsonObject* Json, FJ FString WithoutGuid = PinName.Left(LastUnderscore); // Strip _Index int32 SecondUnderscore; - if (WithoutGuid.FindLastChar(TEXT('_'), SecondUnderscore) && SecondUnderscore > 0) + if (WithoutGuid.FindLastChar(TEXT('_'), SecondUnderscore) && (SecondUnderscore > 0)) { FString IndexStr = WithoutGuid.Mid(SecondUnderscore + 1); if (IndexStr.IsNumeric()) @@ -1023,11 +962,12 @@ void FBlueprintMCPServer::HandleChangeStructNodeType(const FJsonObject* Json, FJ const UEdGraphSchema* Schema = Graph->GetSchema(); if (!Schema) { - return MakeErrorJson(Result, TEXT("Graph schema not found")); + return Helper->MakeErrorJson(Result, TEXT("Graph schema not found")); } // Reconstruct to rebuild pins for the new struct type (use schema version for MinimalAPI compat) Schema->ReconstructNode(*Node); + int32 Reconnected = 0; int32 Failed = 0; TArray> ReconnectDetails; @@ -1056,8 +996,8 @@ void FBlueprintMCPServer::HandleChangeStructNodeType(const FJsonObject* Json, FJ for (UEdGraphPin* Pin : Node->Pins) { if (!Pin || Pin->Direction != OldConn.Direction) continue; - if (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Struct && - Pin->PinType.PinSubCategoryObject == NewStruct) + if ((Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Struct) && + (Pin->PinType.PinSubCategoryObject == NewStruct)) { NewPin = Pin; break; @@ -1096,21 +1036,19 @@ void FBlueprintMCPServer::HandleChangeStructNodeType(const FJsonObject* Json, FJ } } - // Save - bool bSaved = SaveBlueprintPackage(BP); + FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); // Return updated node state - TSharedPtr UpdatedNodeState = SerializeNode(Node); + TSharedPtr UpdatedNodeState = Helper->SerializeNode(Node); Result->SetBoolField(TEXT("success"), true); - Result->SetStringField(TEXT("blueprint"), BlueprintName); + Result->SetStringField(TEXT("blueprint"), Blueprint); Result->SetStringField(TEXT("nodeId"), NodeId); Result->SetStringField(TEXT("newStructType"), NewStruct->GetName()); Result->SetStringField(TEXT("nodeClass"), Node->GetClass()->GetName()); Result->SetNumberField(TEXT("reconnected"), Reconnected); Result->SetNumberField(TEXT("failed"), Failed); Result->SetArrayField(TEXT("reconnectDetails"), ReconnectDetails); - Result->SetBoolField(TEXT("saved"), bSaved); if (UpdatedNodeState.IsValid()) { Result->SetObjectField(TEXT("updatedNode"), UpdatedNodeState); @@ -1193,570 +1131,15 @@ void UMCPHandler_DeleteNode::Handle(const FJsonObject* Json, FJsonObject* Result Result->SetStringField(TEXT("graph"), GraphName); } +// (add_node endpoint removed — use spawn_node instead) + // ============================================================ -// HandleAddNode — create a new node in a blueprint graph +// RenameAsset — rename or move an asset // ============================================================ -void FBlueprintMCPServer::HandleAddNode(const FJsonObject* Json, FJsonObject* Result) +void UMCPHandler_RenameAsset::Handle(const FJsonObject* Json, FJsonObject* Result) { - FString BlueprintName = Json->GetStringField(TEXT("blueprint")); - FString GraphName = Json->GetStringField(TEXT("graph")); - FString NodeType = Json->GetStringField(TEXT("nodeType")); - - if (BlueprintName.IsEmpty() || GraphName.IsEmpty() || NodeType.IsEmpty()) - { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, nodeType")); - } - - int32 PosX = 0, PosY = 0; - if (Json->HasField(TEXT("posX"))) - PosX = (int32)Json->GetNumberField(TEXT("posX")); - if (Json->HasField(TEXT("posY"))) - PosY = (int32)Json->GetNumberField(TEXT("posY")); - - // Load Blueprint - FString LoadError; - UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); - if (!BP) - { - return MakeErrorJson(Result, LoadError); - } - - // Find the target graph (URL decode graph name) - FString DecodedGraphName = UrlDecode(GraphName); - UEdGraph* TargetGraph = nullptr; - TArray AllGraphs; - BP->GetAllGraphs(AllGraphs); - - for (UEdGraph* Graph : AllGraphs) - { - if (Graph && Graph->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase)) - { - TargetGraph = Graph; - break; - } - } - - if (!TargetGraph) - { - TArray> GraphNames; - for (UEdGraph* Graph : AllGraphs) - { - if (Graph) GraphNames.Add(MakeShared(Graph->GetName())); - } - MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName)); - Result->SetArrayField(TEXT("availableGraphs"), GraphNames); - return; - } - - UEdGraphNode* NewNode = nullptr; - - if (NodeType == TEXT("BreakStruct") || NodeType == TEXT("MakeStruct")) - { - FString TypeName = Json->GetStringField(TEXT("typeName")); - if (TypeName.IsEmpty()) - { - return MakeErrorJson(Result, TEXT("Missing required field 'typeName' for BreakStruct/MakeStruct")); - } - - // Find the struct type - FString SearchName = TypeName; - if (SearchName.StartsWith(TEXT("F"))) - SearchName = SearchName.Mid(1); - - UScriptStruct* FoundStruct = FindFirstObject(*SearchName); - if (!FoundStruct) - FoundStruct = FindFirstObject(*TypeName); - if (!FoundStruct) - { - // Broader search - for (TObjectIterator It; It; ++It) - { - if (It->GetName() == SearchName || It->GetName() == TypeName) - { - FoundStruct = *It; - break; - } - } - } - if (!FoundStruct) - { - return MakeErrorJson(Result, FString::Printf(TEXT("Struct type '%s' not found"), *TypeName)); - } - - if (NodeType == TEXT("BreakStruct")) - { - UK2Node_BreakStruct* BreakNode = NewObject(TargetGraph); - BreakNode->StructType = FoundStruct; - BreakNode->NodePosX = PosX; - BreakNode->NodePosY = PosY; - TargetGraph->AddNode(BreakNode, false, false); - BreakNode->AllocateDefaultPins(); - NewNode = BreakNode; - } - else - { - UK2Node_MakeStruct* MakeNode = NewObject(TargetGraph); - MakeNode->StructType = FoundStruct; - MakeNode->NodePosX = PosX; - MakeNode->NodePosY = PosY; - TargetGraph->AddNode(MakeNode, false, false); - MakeNode->AllocateDefaultPins(); - NewNode = MakeNode; - } - } - else if (NodeType == TEXT("CallFunction")) - { - FString FunctionName = Json->GetStringField(TEXT("functionName")); - FString ClassName = Json->GetStringField(TEXT("className")); - - if (FunctionName.IsEmpty()) - { - return MakeErrorJson(Result, TEXT("Missing required field 'functionName' for CallFunction")); - } - - // Find the function - UFunction* TargetFunc = nullptr; - - if (!ClassName.IsEmpty()) - { - // Search in specified class - UClass* TargetClass = nullptr; - for (TObjectIterator It; It; ++It) - { - if (It->GetName() == ClassName || It->GetName() == FString::Printf(TEXT("U%s"), *ClassName)) - { - TargetClass = *It; - break; - } - } - if (TargetClass) - { - TargetFunc = TargetClass->FindFunctionByName(FName(*FunctionName)); - } - } - - if (!TargetFunc) - { - // Search across all classes - for (TObjectIterator It; It; ++It) - { - UFunction* Func = It->FindFunctionByName(FName(*FunctionName)); - if (Func) - { - TargetFunc = Func; - break; - } - } - } - - if (!TargetFunc) - { - return MakeErrorJson(Result, FString::Printf(TEXT("Function '%s' not found%s"), - *FunctionName, ClassName.IsEmpty() ? TEXT("") : *FString::Printf(TEXT(" in class '%s'"), *ClassName))); - } - - UK2Node_CallFunction* CallNode = NewObject(TargetGraph); - CallNode->SetFromFunction(TargetFunc); - CallNode->NodePosX = PosX; - CallNode->NodePosY = PosY; - TargetGraph->AddNode(CallNode, false, false); - CallNode->AllocateDefaultPins(); - NewNode = CallNode; - } - else if (NodeType == TEXT("VariableGet") || NodeType == TEXT("VariableSet")) - { - FString VariableName = Json->GetStringField(TEXT("variableName")); - if (VariableName.IsEmpty()) - { - return MakeErrorJson(Result, TEXT("Missing required field 'variableName' for VariableGet/VariableSet")); - } - - // Verify the variable exists in the blueprint - FName VarFName(*VariableName); - bool bVarFound = false; - for (const FBPVariableDescription& Var : BP->NewVariables) - { - if (Var.VarName == VarFName) - { - bVarFound = true; - break; - } - } - - if (!bVarFound) - { - // Also check inherited properties - if (BP->GeneratedClass) - { - FProperty* Prop = BP->GeneratedClass->FindPropertyByName(VarFName); - if (Prop) - bVarFound = true; - } - } - - if (!bVarFound) - { - return MakeErrorJson(Result, FString::Printf(TEXT("Variable '%s' not found in Blueprint '%s'"), - *VariableName, *BlueprintName)); - } - - if (NodeType == TEXT("VariableGet")) - { - UK2Node_VariableGet* GetNode = NewObject(TargetGraph); - GetNode->VariableReference.SetSelfMember(VarFName); - GetNode->NodePosX = PosX; - GetNode->NodePosY = PosY; - TargetGraph->AddNode(GetNode, false, false); - GetNode->AllocateDefaultPins(); - NewNode = GetNode; - } - else - { - UK2Node_VariableSet* SetNode = NewObject(TargetGraph); - SetNode->VariableReference.SetSelfMember(VarFName); - SetNode->NodePosX = PosX; - SetNode->NodePosY = PosY; - TargetGraph->AddNode(SetNode, false, false); - SetNode->AllocateDefaultPins(); - NewNode = SetNode; - } - } - else if (NodeType == TEXT("DynamicCast")) - { - FString CastTarget = Json->GetStringField(TEXT("castTarget")); - if (CastTarget.IsEmpty()) - { - return MakeErrorJson(Result, TEXT("Missing required field 'castTarget' for DynamicCast")); - } - - // Find the target class (C++ or Blueprint) - UClass* TargetClass = nullptr; - for (TObjectIterator It; It; ++It) - { - FString ClassName = It->GetName(); - if (ClassName == CastTarget || ClassName == CastTarget + TEXT("_C")) - { - TargetClass = *It; - break; - } - } - if (!TargetClass) - { - return MakeErrorJson(Result, FString::Printf(TEXT("Cast target class '%s' not found"), *CastTarget)); - } - - UK2Node_DynamicCast* CastNode = NewObject(TargetGraph); - CastNode->TargetType = TargetClass; - CastNode->NodePosX = PosX; - CastNode->NodePosY = PosY; - TargetGraph->AddNode(CastNode, false, false); - CastNode->AllocateDefaultPins(); - NewNode = CastNode; - } - else if (NodeType == TEXT("OverrideEvent")) - { - FString FunctionName = Json->GetStringField(TEXT("functionName")); - if (FunctionName.IsEmpty()) - { - return MakeErrorJson(Result, TEXT("Missing required field 'functionName' for OverrideEvent")); - } - - if (!BP->ParentClass) - { - return MakeErrorJson(Result, TEXT("Blueprint has no parent class")); - } - - UFunction* Func = BP->ParentClass->FindFunctionByName(FName(*FunctionName)); - if (!Func) - { - return MakeErrorJson(Result, FString::Printf(TEXT("Function '%s' not found on parent class '%s'"), - *FunctionName, *BP->ParentClass->GetName())); - } - - // Check for duplicate override event already in graph - for (UEdGraphNode* ExistingNode : TargetGraph->Nodes) - { - if (UK2Node_Event* ExistingEvent = Cast(ExistingNode)) - { - if (ExistingEvent->bOverrideFunction && - ExistingEvent->EventReference.GetMemberName() == FName(*FunctionName)) - { - // Already exists — return it with alreadyExists flag - TSharedPtr NodeState = SerializeNode(ExistingEvent); - Result->SetBoolField(TEXT("success"), true); - Result->SetBoolField(TEXT("alreadyExists"), true); - Result->SetStringField(TEXT("blueprint"), BlueprintName); - Result->SetStringField(TEXT("graph"), DecodedGraphName); - Result->SetStringField(TEXT("nodeType"), NodeType); - Result->SetStringField(TEXT("nodeId"), ExistingEvent->NodeGuid.ToString()); - if (NodeState.IsValid()) - { - Result->SetObjectField(TEXT("node"), NodeState); - } - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: OverrideEvent '%s' already exists in '%s', returning existing node"), - *FunctionName, *BlueprintName); - return; - } - } - } - - UK2Node_Event* EventNode = NewObject(TargetGraph); - EventNode->EventReference.SetFromField(Func, false); - EventNode->bOverrideFunction = true; - EventNode->NodePosX = PosX; - EventNode->NodePosY = PosY; - TargetGraph->AddNode(EventNode, false, false); - EventNode->AllocateDefaultPins(); - NewNode = EventNode; - } - else if (NodeType == TEXT("CallParentFunction")) - { - FString FunctionName = Json->GetStringField(TEXT("functionName")); - if (FunctionName.IsEmpty()) - { - return MakeErrorJson(Result, TEXT("Missing required field 'functionName' for CallParentFunction")); - } - - if (!BP->ParentClass) - { - return MakeErrorJson(Result, TEXT("Blueprint has no parent class")); - } - - UFunction* Func = BP->ParentClass->FindFunctionByName(FName(*FunctionName)); - if (!Func) - { - return MakeErrorJson(Result, FString::Printf(TEXT("Function '%s' not found on parent class '%s'"), - *FunctionName, *BP->ParentClass->GetName())); - } - - UK2Node_CallParentFunction* ParentCallNode = NewObject(TargetGraph); - ParentCallNode->SetFromFunction(Func); - ParentCallNode->NodePosX = PosX; - ParentCallNode->NodePosY = PosY; - TargetGraph->AddNode(ParentCallNode, false, false); - ParentCallNode->AllocateDefaultPins(); - NewNode = ParentCallNode; - } - else if (NodeType == TEXT("Branch")) - { - UK2Node_IfThenElse* BranchNode = NewObject(TargetGraph); - BranchNode->NodePosX = PosX; - BranchNode->NodePosY = PosY; - TargetGraph->AddNode(BranchNode, false, false); - BranchNode->AllocateDefaultPins(); - NewNode = BranchNode; - } - else if (NodeType == TEXT("Sequence")) - { - UK2Node_ExecutionSequence* SeqNode = NewObject(TargetGraph); - SeqNode->NodePosX = PosX; - SeqNode->NodePosY = PosY; - TargetGraph->AddNode(SeqNode, false, false); - SeqNode->AllocateDefaultPins(); - NewNode = SeqNode; - } - else if (NodeType == TEXT("CustomEvent")) - { - FString EventName = Json->GetStringField(TEXT("eventName")); - if (EventName.IsEmpty()) - { - return MakeErrorJson(Result, TEXT("Missing required field 'eventName' for CustomEvent")); - } - - UK2Node_CustomEvent* EventNode = NewObject(TargetGraph); - EventNode->CustomFunctionName = FName(*EventName); - EventNode->NodePosX = PosX; - EventNode->NodePosY = PosY; - TargetGraph->AddNode(EventNode, false, false); - EventNode->AllocateDefaultPins(); - NewNode = EventNode; - } - else if (NodeType == TEXT("ForEachLoop") || NodeType == TEXT("ForLoop") || NodeType == TEXT("ForLoopWithBreak") || NodeType == TEXT("WhileLoop")) - { - // These are all macro instances from the engine standard macro library - FString MacroName; - if (NodeType == TEXT("ForEachLoop")) MacroName = TEXT("ForEachLoop"); - else if (NodeType == TEXT("ForLoop")) MacroName = TEXT("ForLoop"); - else if (NodeType == TEXT("ForLoopWithBreak")) MacroName = TEXT("ForLoopWithBreak"); - else MacroName = TEXT("WhileLoop"); - - // Load the standard macros Blueprint from the engine - UBlueprint* StandardMacros = Cast(StaticLoadObject( - UBlueprint::StaticClass(), nullptr, - TEXT("/Engine/EditorBlueprintResources/StandardMacros.StandardMacros"))); - - UEdGraph* MacroGraph = nullptr; - if (StandardMacros) - { - for (UEdGraph* Graph : StandardMacros->MacroGraphs) - { - if (Graph && Graph->GetName() == MacroName) - { - MacroGraph = Graph; - break; - } - } - } - - if (!MacroGraph) - { - return MakeErrorJson(Result, FString::Printf( - TEXT("Standard macro '%s' not found. Ensure the engine standard macros are loaded."), *MacroName)); - } - - UK2Node_MacroInstance* MacroNode = NewObject(TargetGraph); - MacroNode->SetMacroGraph(MacroGraph); - MacroNode->NodePosX = PosX; - MacroNode->NodePosY = PosY; - TargetGraph->AddNode(MacroNode, false, false); - MacroNode->AllocateDefaultPins(); - NewNode = MacroNode; - } - else if (NodeType == TEXT("SpawnActorFromClass")) - { - FString ClassName = Json->GetStringField(TEXT("actorClass")); - // actorClass is optional — if not provided, user can set it via the class pin later - - UClass* ActorClass = nullptr; - if (!ClassName.IsEmpty()) - { - for (TObjectIterator It; It; ++It) - { - if ((It->GetName() == ClassName || It->GetName() == ClassName + TEXT("_C")) && It->IsChildOf(AActor::StaticClass())) - { - ActorClass = *It; - break; - } - } - if (!ActorClass) - { - return MakeErrorJson(Result, FString::Printf(TEXT("Actor class '%s' not found"), *ClassName)); - } - } - - UK2Node_SpawnActorFromClass* SpawnNode = NewObject(TargetGraph); - SpawnNode->NodePosX = PosX; - SpawnNode->NodePosY = PosY; - TargetGraph->AddNode(SpawnNode, false, false); - SpawnNode->AllocateDefaultPins(); - if (ActorClass) - { - UEdGraphPin* ClassPin = SpawnNode->GetClassPin(); - if (ClassPin) - { - ClassPin->DefaultObject = ActorClass; - if (const UEdGraphSchema* SpawnSchema = TargetGraph->GetSchema()) - { - SpawnSchema->ReconstructNode(*SpawnNode); - } - } - } - NewNode = SpawnNode; - } - else if (NodeType == TEXT("Select")) - { - UK2Node_Select* SelectNode = NewObject(TargetGraph); - SelectNode->NodePosX = PosX; - SelectNode->NodePosY = PosY; - TargetGraph->AddNode(SelectNode, false, false); - SelectNode->AllocateDefaultPins(); - NewNode = SelectNode; - } - else if (NodeType == TEXT("Comment")) - { - FString CommentText = Json->GetStringField(TEXT("comment")); - if (CommentText.IsEmpty()) - { - CommentText = TEXT("Comment"); - } - int32 Width = 400; - int32 Height = 200; - if (Json->HasField(TEXT("width"))) - { - Width = FMath::Max(64, Json->GetIntegerField(TEXT("width"))); - } - if (Json->HasField(TEXT("height"))) - { - Height = FMath::Max(64, Json->GetIntegerField(TEXT("height"))); - } - - UEdGraphNode_Comment* CommentNode = NewObject(TargetGraph); - CommentNode->NodeComment = CommentText; - CommentNode->NodePosX = PosX; - CommentNode->NodePosY = PosY; - CommentNode->NodeWidth = Width; - CommentNode->NodeHeight = Height; - TargetGraph->AddNode(CommentNode, false, false); - CommentNode->AllocateDefaultPins(); - NewNode = CommentNode; - } - else if (NodeType == TEXT("Reroute")) - { - UK2Node_Knot* KnotNode = NewObject(TargetGraph); - KnotNode->NodePosX = PosX; - KnotNode->NodePosY = PosY; - TargetGraph->AddNode(KnotNode, false, false); - KnotNode->AllocateDefaultPins(); - NewNode = KnotNode; - } - else - { - return MakeErrorJson(Result, FString::Printf( - TEXT("Unsupported nodeType '%s'. Supported: BreakStruct, MakeStruct, CallFunction, VariableGet, VariableSet, DynamicCast, OverrideEvent, CallParentFunction, Branch, Sequence, CustomEvent, ForEachLoop, ForLoop, ForLoopWithBreak, WhileLoop, SpawnActorFromClass, Select, Comment, Reroute"), - *NodeType)); - } - - if (!NewNode) - { - return MakeErrorJson(Result, TEXT("Failed to create node")); - } - - // Ensure node has a valid GUID (PostInitProperties may skip it in some contexts) - if (!NewNode->NodeGuid.IsValid()) - { - NewNode->CreateNewGuid(); - } - - // Mark as modified - FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP); - - // Save - bool bSaved = SaveBlueprintPackage(BP); - - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added %s node '%s' in graph '%s' of '%s', save %s"), - *NodeType, *NewNode->NodeGuid.ToString(), *DecodedGraphName, *BlueprintName, - bSaved ? TEXT("succeeded") : TEXT("failed")); - - // Serialize the new node (includes GUID and pin list) - TSharedPtr NodeState = SerializeNode(NewNode); - - Result->SetBoolField(TEXT("success"), true); - Result->SetStringField(TEXT("blueprint"), BlueprintName); - Result->SetStringField(TEXT("graph"), DecodedGraphName); - Result->SetStringField(TEXT("nodeType"), NodeType); - Result->SetStringField(TEXT("nodeId"), NewNode->NodeGuid.ToString()); - Result->SetBoolField(TEXT("saved"), bSaved); - if (NodeState.IsValid()) - { - Result->SetObjectField(TEXT("node"), NodeState); - } -} - -// ============================================================ -// HandleRenameAsset — rename or move an asset -// ============================================================ - -void FBlueprintMCPServer::HandleRenameAsset(const FJsonObject* Json, FJsonObject* Result) -{ - FString AssetPath = Json->GetStringField(TEXT("assetPath")); - FString NewPath = Json->GetStringField(TEXT("newPath")); - - if (AssetPath.IsEmpty() || NewPath.IsEmpty()) - { - return MakeErrorJson(Result, TEXT("Missing required fields: assetPath, newPath")); - } + MCPHelper* Helper = MCPHelper::Get(); UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Renaming asset '%s' -> '%s'"), *AssetPath, *NewPath); @@ -1768,16 +1151,16 @@ void FBlueprintMCPServer::HandleRenameAsset(const FJsonObject* Json, FJsonObject TArray RenameData; // We need to load the asset to get the object - FAssetData* FoundAsset = FindAnyAsset(AssetPath); + FAssetData* FoundAsset = Helper->FindAnyAsset(AssetPath); if (!FoundAsset) { - return MakeErrorJson(Result, FString::Printf(TEXT("Asset '%s' not found. Checked Blueprints, Materials, Material Instances, and Material Functions."), *AssetPath)); + return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Asset '%s' not found. Checked Blueprints, Materials, Material Instances, and Material Functions."), *AssetPath)); } UObject* AssetObj = FoundAsset->GetAsset(); if (!AssetObj) { - return MakeErrorJson(Result, FString::Printf(TEXT("Failed to load asset '%s'"), *AssetPath)); + return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Failed to load asset '%s'"), *AssetPath)); } // Parse new path into package path and asset name @@ -1809,14 +1192,14 @@ void FBlueprintMCPServer::HandleRenameAsset(const FJsonObject* Json, FJsonObject { // Update all cached asset lists — re-scan to pick up the new path FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked("AssetRegistry"); - AllBlueprintAssets.Empty(); - ARM.Get().GetAssetsByClass(UBlueprint::StaticClass()->GetClassPathName(), AllBlueprintAssets, true); - AllMaterialAssets.Empty(); - ARM.Get().GetAssetsByClass(UMaterial::StaticClass()->GetClassPathName(), AllMaterialAssets, true); - AllMaterialInstanceAssets.Empty(); - ARM.Get().GetAssetsByClass(UMaterialInstanceConstant::StaticClass()->GetClassPathName(), AllMaterialInstanceAssets, true); - AllMaterialFunctionAssets.Empty(); - ARM.Get().GetAssetsByClass(UMaterialFunction::StaticClass()->GetClassPathName(), AllMaterialFunctionAssets, true); + Helper->AllBlueprintAssets.Empty(); + ARM.Get().GetAssetsByClass(UBlueprint::StaticClass()->GetClassPathName(), Helper->AllBlueprintAssets, true); + Helper->AllMaterialAssets.Empty(); + ARM.Get().GetAssetsByClass(UMaterial::StaticClass()->GetClassPathName(), Helper->AllMaterialAssets, true); + Helper->AllMaterialInstanceAssets.Empty(); + ARM.Get().GetAssetsByClass(UMaterialInstanceConstant::StaticClass()->GetClassPathName(), Helper->AllMaterialInstanceAssets, true); + Helper->AllMaterialFunctionAssets.Empty(); + ARM.Get().GetAssetsByClass(UMaterialFunction::StaticClass()->GetClassPathName(), Helper->AllMaterialFunctionAssets, true); } UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Rename %s"), bSuccess ? TEXT("succeeded") : TEXT("failed")); @@ -1828,48 +1211,41 @@ void FBlueprintMCPServer::HandleRenameAsset(const FJsonObject* Json, FJsonObject Result->SetStringField(TEXT("newAssetName"), NewAssetName); if (!bSuccess) { - MakeErrorJson(Result, TEXT("Asset rename failed. The target path may be invalid or a conflicting asset may exist.")); + Helper->MakeErrorJson(Result, TEXT("Asset rename failed. The target path may be invalid or a conflicting asset may exist.")); } } // ============================================================ -// Set Blueprint Default Property Value +// SetBlueprintDefault — set a default property value on a Blueprint CDO // ============================================================ -void FBlueprintMCPServer::HandleSetBlueprintDefault(const FJsonObject* Json, FJsonObject* Result) +void UMCPHandler_SetBlueprintDefault::Handle(const FJsonObject* Json, FJsonObject* Result) { - FString BlueprintName = Json->GetStringField(TEXT("blueprint")); - FString PropertyName = Json->GetStringField(TEXT("property")); - FString Value = Json->GetStringField(TEXT("value")); - - if (BlueprintName.IsEmpty() || PropertyName.IsEmpty()) - { - return MakeErrorJson(Result, TEXT("Missing required fields: blueprint, property")); - } + MCPHelper* Helper = MCPHelper::Get(); // Load Blueprint FString LoadError; - UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError); + UBlueprint* BP = Helper->LoadBlueprintByName(Blueprint, LoadError); if (!BP) { - return MakeErrorJson(Result, LoadError); + return Helper->MakeErrorJson(Result, LoadError); } if (!BP->GeneratedClass) { - return MakeErrorJson(Result, TEXT("Blueprint has no GeneratedClass")); + return Helper->MakeErrorJson(Result, TEXT("Blueprint has no GeneratedClass")); } UObject* CDO = BP->GeneratedClass->GetDefaultObject(); if (!CDO) { - return MakeErrorJson(Result, TEXT("Could not get Class Default Object")); + return Helper->MakeErrorJson(Result, TEXT("Could not get Class Default Object")); } - FProperty* Prop = BP->GeneratedClass->FindPropertyByName(*PropertyName); + FProperty* Prop = BP->GeneratedClass->FindPropertyByName(*Property); if (!Prop) { - return MakeErrorJson(Result, FString::Printf(TEXT("Property '%s' not found on '%s'"), *PropertyName, *BlueprintName)); + return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Property '%s' not found on '%s'"), *Property, *Blueprint)); } FString OldValue; @@ -1901,7 +1277,7 @@ void FBlueprintMCPServer::HandleSetBlueprintDefault(const FJsonObject* Json, FJs if (!ResolvedClass) { FString BPLoadError; - UBlueprint* ValueBP = LoadBlueprintByName(Value, BPLoadError); + UBlueprint* ValueBP = Helper->LoadBlueprintByName(Value, BPLoadError); if (ValueBP && ValueBP->GeneratedClass) { ResolvedClass = ValueBP->GeneratedClass; @@ -1910,7 +1286,7 @@ void FBlueprintMCPServer::HandleSetBlueprintDefault(const FJsonObject* Json, FJs if (!ResolvedClass) { - return MakeErrorJson(Result, FString::Printf(TEXT("Could not resolve '%s' to a class"), *Value)); + return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Could not resolve '%s' to a class"), *Value)); } // Validate meta class compatibility @@ -1919,9 +1295,9 @@ void FBlueprintMCPServer::HandleSetBlueprintDefault(const FJsonObject* Json, FJs UClass* MetaClass = ClassProp->MetaClass; if (MetaClass && !ResolvedClass->IsChildOf(MetaClass)) { - return MakeErrorJson(Result, FString::Printf( + return Helper->MakeErrorJson(Result, FString::Printf( TEXT("'%s' is not a subclass of '%s' (required by property '%s')"), - *ResolvedClass->GetName(), *MetaClass->GetName(), *PropertyName)); + *ResolvedClass->GetName(), *MetaClass->GetName(), *Property)); } ClassProp->SetPropertyValue_InContainer(CDO, ResolvedClass); } @@ -1941,7 +1317,7 @@ void FBlueprintMCPServer::HandleSetBlueprintDefault(const FJsonObject* Json, FJs // Try loading as a Blueprint asset FString ObjLoadError; - UBlueprint* ValueBP = LoadBlueprintByName(Value, ObjLoadError); + UBlueprint* ValueBP = Helper->LoadBlueprintByName(Value, ObjLoadError); if (ValueBP && ValueBP->GeneratedClass) { ResolvedObj = ValueBP->GeneratedClass->GetDefaultObject(); @@ -1949,7 +1325,7 @@ void FBlueprintMCPServer::HandleSetBlueprintDefault(const FJsonObject* Json, FJs if (!ResolvedObj) { - return MakeErrorJson(Result, FString::Printf(TEXT("Could not resolve '%s' to an object"), *Value)); + return Helper->MakeErrorJson(Result, FString::Printf(TEXT("Could not resolve '%s' to an object"), *Value)); } ObjProp->SetPropertyValue_InContainer(CDO, ResolvedObj); @@ -1967,15 +1343,15 @@ void FBlueprintMCPServer::HandleSetBlueprintDefault(const FJsonObject* Json, FJs } else { - return MakeErrorJson(Result, FString::Printf( + return Helper->MakeErrorJson(Result, FString::Printf( TEXT("Failed to set property '%s' to '%s' — value could not be parsed for type '%s'"), - *PropertyName, *Value, *Prop->GetCPPType())); + *Property, *Value, *Prop->GetCPPType())); } } if (!bSuccess) { - return MakeErrorJson(Result, TEXT("Failed to set property value")); + return Helper->MakeErrorJson(Result, TEXT("Failed to set property value")); } // Mark modified and save @@ -1983,14 +1359,14 @@ void FBlueprintMCPServer::HandleSetBlueprintDefault(const FJsonObject* Json, FJs BP->Modify(); FKismetEditorUtilities::CompileBlueprint(BP); - bool bSaved = SaveBlueprintPackage(BP); + bool bSaved = Helper->SaveBlueprintPackage(BP); UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Set '%s.%s' from '%s' to '%s' (saved: %s)"), - *BlueprintName, *PropertyName, *OldValue, *ActualNewValue, bSaved ? TEXT("true") : TEXT("false")); + *Blueprint, *Property, *OldValue, *ActualNewValue, bSaved ? TEXT("true") : TEXT("false")); Result->SetBoolField(TEXT("success"), true); - Result->SetStringField(TEXT("blueprint"), BlueprintName); - Result->SetStringField(TEXT("property"), PropertyName); + Result->SetStringField(TEXT("blueprint"), Blueprint); + Result->SetStringField(TEXT("property"), Property); Result->SetStringField(TEXT("oldValue"), OldValue); Result->SetStringField(TEXT("newValue"), ActualNewValue); Result->SetStringField(TEXT("propertyType"), Prop->GetCPPType()); @@ -2351,26 +1727,16 @@ struct FNodeActionSearch }; // ============================================================ -// HandleSearchNodeActions — search the blueprint action database +// SearchNodeActions — search the blueprint action database // for spawners matching a query string (same pool as the right-click menu) // ============================================================ -void FBlueprintMCPServer::HandleSearchNodeActions(const FJsonObject* Json, FJsonObject* Result) +void UMCPHandler_SearchNodeActions::Handle(const FJsonObject* Json, FJsonObject* Result) { - FString Query = Json->GetStringField(TEXT("query")); - if (Query.IsEmpty()) - { - return MakeErrorJson(Result, TEXT("Missing required field: query")); - } - - int32 MaxResults = 50; - if (Json->HasField(TEXT("maxResults"))) - { - MaxResults = FMath::Clamp(Json->GetIntegerField(TEXT("maxResults")), 1, 500); - } + int32 ClampedMax = FMath::Clamp(MaxResults, 1, 500); TArray FullNames; - FNodeActionSearch::Find(Query, MaxResults, FullNames); + FNodeActionSearch::Find(Query, ClampedMax, FullNames); TArray> ResultArray; for (const FString& Name : FullNames) diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPServer.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPServer.cpp index cfe0f028..968aa0bf 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPServer.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPServer.cpp @@ -367,23 +367,6 @@ static int32 TrySavePackageSEH( } } -static void RefreshAllNodesInner(UBlueprint* BP) -{ - FBlueprintEditorUtils::RefreshAllNodes(BP); -} - -int32 TryRefreshAllNodesSEH(UBlueprint* BP) -{ - __try - { - RefreshAllNodesInner(BP); - return 0; - } - __except (1) - { - return -1; - } -} // Inner: create expression, register in material, and trigger PostEditChange. // All of this may crash for classes that are effectively abstract. @@ -601,13 +584,13 @@ bool FBlueprintMCPServer::Start(int32 InPort, bool bEditorMode) Router->BindRoute(FHttpPath(TEXT("/api/references")), EHttpServerRequestVerbs::VERB_GET, QueuedHandler(TEXT("references"))); Router->BindRoute(FHttpPath(TEXT("/api/replace-function-calls")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("replaceFunctionCalls"))); + QueuedHandler(TEXT("replace_function_calls"))); Router->BindRoute(FHttpPath(TEXT("/api/change-variable-type")), EHttpServerRequestVerbs::VERB_POST, QueuedHandler(TEXT("changeVariableType"))); Router->BindRoute(FHttpPath(TEXT("/api/change-function-param-type")), EHttpServerRequestVerbs::VERB_POST, QueuedHandler(TEXT("changeFunctionParamType"))); Router->BindRoute(FHttpPath(TEXT("/api/delete-asset")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("deleteAsset"))); + QueuedHandler(TEXT("delete_asset"))); Router->BindRoute(FHttpPath(TEXT("/api/test-save")), EHttpServerRequestVerbs::VERB_GET, QueuedHandler(TEXT("testSave"))); Router->BindRoute(FHttpPath(TEXT("/api/connect-pins")), EHttpServerRequestVerbs::VERB_POST, @@ -615,7 +598,7 @@ bool FBlueprintMCPServer::Start(int32 InPort, bool bEditorMode) Router->BindRoute(FHttpPath(TEXT("/api/disconnect-pin")), EHttpServerRequestVerbs::VERB_POST, QueuedHandler(TEXT("disconnect_pin"))); Router->BindRoute(FHttpPath(TEXT("/api/refresh-all-nodes")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("refreshAllNodes"))); + QueuedHandler(TEXT("refresh_all_nodes"))); Router->BindRoute(FHttpPath(TEXT("/api/set-pin-default")), EHttpServerRequestVerbs::VERB_POST, QueuedHandler(TEXT("set_pin_default"))); Router->BindRoute(FHttpPath(TEXT("/api/move-node")), EHttpServerRequestVerbs::VERB_POST, @@ -635,7 +618,7 @@ bool FBlueprintMCPServer::Start(int32 InPort, bool bEditorMode) Router->BindRoute(FHttpPath(TEXT("/api/list-properties")), EHttpServerRequestVerbs::VERB_POST, QueuedHandler(TEXT("listProperties"))); Router->BindRoute(FHttpPath(TEXT("/api/change-struct-node-type")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("changeStructNodeType"))); + QueuedHandler(TEXT("change_struct_node_type"))); Router->BindRoute(FHttpPath(TEXT("/api/remove-function-parameter")), EHttpServerRequestVerbs::VERB_POST, QueuedHandler(TEXT("removeFunctionParameter"))); Router->BindRoute(FHttpPath(TEXT("/api/delete-node")), EHttpServerRequestVerbs::VERB_POST, @@ -648,18 +631,16 @@ bool FBlueprintMCPServer::Start(int32 InPort, bool bEditorMode) QueuedHandler(TEXT("validateBlueprint"))); Router->BindRoute(FHttpPath(TEXT("/api/validate-all-blueprints")), EHttpServerRequestVerbs::VERB_POST, QueuedHandler(TEXT("validateAllBlueprints"))); - Router->BindRoute(FHttpPath(TEXT("/api/add-node")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("addNode"))); - Router->BindRoute(FHttpPath(TEXT("/api/search-node-actions")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("searchNodeActions"))); +Router->BindRoute(FHttpPath(TEXT("/api/search-node-actions")), EHttpServerRequestVerbs::VERB_POST, + QueuedHandler(TEXT("search_node_actions"))); Router->BindRoute(FHttpPath(TEXT("/api/spawn-node")), EHttpServerRequestVerbs::VERB_POST, QueuedHandler(TEXT("spawn_node"))); Router->BindRoute(FHttpPath(TEXT("/api/rename-asset")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("renameAsset"))); + QueuedHandler(TEXT("rename_asset"))); Router->BindRoute(FHttpPath(TEXT("/api/reparent-blueprint")), EHttpServerRequestVerbs::VERB_POST, QueuedHandler(TEXT("reparentBlueprint"))); Router->BindRoute(FHttpPath(TEXT("/api/set-blueprint-default")), EHttpServerRequestVerbs::VERB_POST, - QueuedHandler(TEXT("setBlueprintDefault"))); + QueuedHandler(TEXT("set_blueprint_default"))); Router->BindRoute(FHttpPath(TEXT("/api/create-blueprint")), EHttpServerRequestVerbs::VERB_POST, QueuedHandler(TEXT("createBlueprint"))); Router->BindRoute(FHttpPath(TEXT("/api/create-graph")), EHttpServerRequestVerbs::VERB_POST, @@ -977,25 +958,24 @@ void FBlueprintMCPServer::RegisterHandlers() { // Mutation endpoints — wrapped in undo transactions by ProcessOneRequest() MutationEndpoints = { - TEXT("replaceFunctionCalls"), + TEXT("replace_function_calls"), TEXT("changeVariableType"), TEXT("changeFunctionParamType"), TEXT("removeFunctionParameter"), - TEXT("deleteAsset"), + TEXT("delete_asset"), TEXT("connect_pins"), TEXT("disconnect_pin"), - TEXT("refreshAllNodes"), + TEXT("refresh_all_nodes"), TEXT("set_pin_default"), TEXT("move_node"), - TEXT("changeStructNodeType"), + TEXT("change_struct_node_type"), TEXT("delete_node"), TEXT("duplicate_nodes"), - TEXT("addNode"), TEXT("spawn_node"), TEXT("set_node_comment"), - TEXT("renameAsset"), + TEXT("rename_asset"), TEXT("reparentBlueprint"), - TEXT("setBlueprintDefault"), + TEXT("set_blueprint_default"), TEXT("createBlueprint"), TEXT("createGraph"), TEXT("deleteGraph"), @@ -1050,14 +1030,14 @@ void FBlueprintMCPServer::RegisterHandlers() H(TEXT("references"), &FBlueprintMCPServer::HandleFindReferences); H(TEXT("testSave"), &FBlueprintMCPServer::HandleTestSave); H(TEXT("searchByType"), &FBlueprintMCPServer::HandleSearchByType); - H(TEXT("replaceFunctionCalls"), &FBlueprintMCPServer::HandleReplaceFunctionCalls); + // replace_function_calls is now handled by UMCPHandler_ReplaceFunctionCalls (new-style registry) H(TEXT("changeVariableType"), &FBlueprintMCPServer::HandleChangeVariableType); H(TEXT("changeFunctionParamType"), &FBlueprintMCPServer::HandleChangeFunctionParamType); H(TEXT("removeFunctionParameter"), &FBlueprintMCPServer::HandleRemoveFunctionParameter); - H(TEXT("deleteAsset"), &FBlueprintMCPServer::HandleDeleteAsset); + // delete_asset is now handled by UMCPHandler_DeleteAsset (new-style registry) // 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); + // refresh_all_nodes is now handled by UMCPHandler_RefreshAllNodes (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) // get_node_comment is now handled by UMCPHandler_GetNodeComment (new-style registry) @@ -1067,17 +1047,16 @@ void FBlueprintMCPServer::RegisterHandlers() H(TEXT("listClasses"), &FBlueprintMCPServer::HandleListClasses); H(TEXT("listFunctions"), &FBlueprintMCPServer::HandleListFunctions); H(TEXT("listProperties"), &FBlueprintMCPServer::HandleListProperties); - H(TEXT("changeStructNodeType"), &FBlueprintMCPServer::HandleChangeStructNodeType); + // change_struct_node_type is now handled by UMCPHandler_ChangeStructNodeType (new-style registry) // 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); - H(TEXT("addNode"), &FBlueprintMCPServer::HandleAddNode); - H(TEXT("searchNodeActions"), &FBlueprintMCPServer::HandleSearchNodeActions); +// search_node_actions is now handled by UMCPHandler_SearchNodeActions (new-style registry) // spawn_node is now handled by UMCPHandler_SpawnNode (new-style registry) - H(TEXT("renameAsset"), &FBlueprintMCPServer::HandleRenameAsset); + // rename_asset is now handled by UMCPHandler_RenameAsset (new-style registry) H(TEXT("reparentBlueprint"), &FBlueprintMCPServer::HandleReparentBlueprint); - H(TEXT("setBlueprintDefault"), &FBlueprintMCPServer::HandleSetBlueprintDefault); + // set_blueprint_default is now handled by UMCPHandler_SetBlueprintDefault (new-style registry) H(TEXT("createBlueprint"), &FBlueprintMCPServer::HandleCreateBlueprint); H(TEXT("createGraph"), &FBlueprintMCPServer::HandleCreateGraph); H(TEXT("deleteGraph"), &FBlueprintMCPServer::HandleDeleteGraph); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintMCPHandlers_Mutation.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintMCPHandlers_Mutation.h index 5e4386ce..f2b0eaee 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintMCPHandlers_Mutation.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintMCPHandlers_Mutation.h @@ -285,3 +285,158 @@ public: virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override; }; + +UCLASS(meta=(ToolName="replace_function_calls")) +class UMCPHandler_ReplaceFunctionCalls : public UMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Old class name to match")) + FString OldClass; + + UPROPERTY(meta=(Description="New class name to redirect to")) + FString NewClass; + + UPROPERTY(meta=(Optional, Description="If true, report what would change without modifying")) + bool DryRun = false; + + virtual FString GetDescription() const override + { + return TEXT("Redirect function call nodes from one class to another. " + "Supports dry-run to preview impact before applying."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override; +}; + +UCLASS(meta=(ToolName="delete_asset")) +class UMCPHandler_DeleteAsset : public UMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Package path of the asset to delete")) + FString AssetPath; + + UPROPERTY(meta=(Optional, Description="If true, skip reference check and force delete")) + bool Force = false; + + virtual FString GetDescription() const override + { + return TEXT("Delete a .uasset after verifying no references. " + "Use force=true to skip the reference check."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override; +}; + +UCLASS(meta=(ToolName="refresh_all_nodes")) +class UMCPHandler_RefreshAllNodes : public UMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + virtual FString GetDescription() const override + { + return TEXT("Refresh all nodes in a Blueprint, removing orphaned pins. " + "Reports compiler warnings and errors."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override; +}; + +UCLASS(meta=(ToolName="change_struct_node_type")) +class UMCPHandler_ChangeStructNodeType : public UMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Node GUID of the BreakStruct or MakeStruct node")) + FString NodeId; + + UPROPERTY(meta=(Description="New struct type name (e.g. 'FVector', 'Vector')")) + FString NewType; + + virtual FString GetDescription() const override + { + return TEXT("Change the struct type on a BreakStruct or MakeStruct node. " + "Attempts to reconnect matching pins after the type change."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override; +}; + +UCLASS(meta=(ToolName="rename_asset")) +class UMCPHandler_RenameAsset : public UMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Current package path of the asset")) + FString AssetPath; + + UPROPERTY(meta=(Description="New package path or new asset name")) + FString NewPath; + + virtual FString GetDescription() const override + { + return TEXT("Rename or move an asset with reference fixup."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override; +}; + +UCLASS(meta=(ToolName="set_blueprint_default")) +class UMCPHandler_SetBlueprintDefault : public UMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Blueprint name or package path")) + FString Blueprint; + + UPROPERTY(meta=(Description="Property name on the Class Default Object")) + FString Property; + + UPROPERTY(meta=(Description="New value (parsed according to property type)")) + FString Value; + + virtual FString GetDescription() const override + { + return TEXT("Set a default property value on a Blueprint's Class Default Object. " + "Handles class references, object references, and simple types."); + } + + virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override; +}; + +UCLASS(meta=(ToolName="search_node_actions")) +class UMCPHandler_SearchNodeActions : public UMCPHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Search query string")) + FString Query; + + UPROPERTY(meta=(Optional, Description="Maximum number of results (default 50, max 500)")) + int32 MaxResults = 50; + + virtual FString GetDescription() const override + { + return TEXT("Search the Blueprint action database for node spawners matching a query. " + "Returns full action names for use with spawn_node."); + } + + 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 1bc9cba9..be31d100 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintMCPServer.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/BlueprintMCPServer.h @@ -97,6 +97,13 @@ public: /** Number of indexed Material Instance assets. */ int32 GetMaterialInstanceCount() const { return AllMaterialInstanceAssets.Num(); } + // ----- Cached asset lists (accessed by handlers) ----- + TArray AllBlueprintAssets; + TArray AllMapAssets; + TArray AllMaterialAssets; + TArray AllMaterialInstanceAssets; + TArray AllMaterialFunctionAssets; + private: // ----- Request dispatch ----- using FRequestHandler = TFunction; @@ -115,11 +122,6 @@ private: }; TQueue> RequestQueue; - TArray AllBlueprintAssets; - TArray AllMapAssets; - TArray AllMaterialAssets; - TArray AllMaterialInstanceAssets; - TArray AllMaterialFunctionAssets; int32 Port = 9847; bool bRunning = false; bool bIsEditor = false; @@ -136,21 +138,14 @@ private: void HandleSearchByType(const FJsonObject* Json, FJsonObject* Result); // ----- Request handlers (write) ----- - void HandleReplaceFunctionCalls(const FJsonObject* Json, FJsonObject* Result); void HandleChangeVariableType(const FJsonObject* Json, FJsonObject* Result); void HandleChangeFunctionParamType(const FJsonObject* Json, FJsonObject* Result); void HandleRemoveFunctionParameter(const FJsonObject* Json, FJsonObject* Result); - void HandleDeleteAsset(const FJsonObject* Json, FJsonObject* Result); - void HandleAddNode(const FJsonObject* Json, FJsonObject* Result); - void HandleRenameAsset(const FJsonObject* Json, FJsonObject* Result); // ----- Validation (read-only, no save) ----- void HandleValidateBlueprint(const FJsonObject* Json, FJsonObject* Result); void HandleValidateAllBlueprints(const FJsonObject* Json, FJsonObject* Result); - // ----- Pin manipulation (write) ----- - void HandleRefreshAllNodes(const FJsonObject* Json, FJsonObject* Result); - // ----- Pin introspection (read-only) ----- void HandleGetPinInfo(const FJsonObject* Json, FJsonObject* Result); void HandleCheckPinCompatibility(const FJsonObject* Json, FJsonObject* Result); @@ -160,9 +155,6 @@ private: void HandleListFunctions(const FJsonObject* Json, FJsonObject* Result); void HandleListProperties(const FJsonObject* Json, FJsonObject* Result); - // ----- Struct node manipulation (write) ----- - void HandleChangeStructNodeType(const FJsonObject* Json, FJsonObject* Result); - // ----- Reparent ----- void HandleReparentBlueprint(const FJsonObject* Json, FJsonObject* Result); @@ -198,12 +190,6 @@ private: void HandleRemoveComponent(const FJsonObject* Json, FJsonObject* Result); void HandleListComponents(const FJsonObject* Json, FJsonObject* Result); - // ----- Property defaults ----- - void HandleSetBlueprintDefault(const FJsonObject* Json, FJsonObject* Result); - - // ----- Generic node spawning via action database ----- - void HandleSearchNodeActions(const FJsonObject* Json, FJsonObject* Result); - // ----- Diagnostic ----- void HandleTestSave(const FJsonObject* Json, FJsonObject* Result);