MCP Backup Restore and remove undo

This commit is contained in:
2026-03-08 04:18:55 -04:00
parent 730396753c
commit d9072cf551
3 changed files with 96 additions and 71 deletions

View File

@@ -12,6 +12,7 @@
#include "AssetRegistry/IAssetRegistry.h" #include "AssetRegistry/IAssetRegistry.h"
#include "AssetToolsModule.h" #include "AssetToolsModule.h"
#include "IAssetTools.h" #include "IAssetTools.h"
#include "FileHelpers.h"
#include "MCPHandlers_AssetMutation.generated.h" #include "MCPHandlers_AssetMutation.generated.h"
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -241,3 +242,97 @@ public:
} }
} }
}; };
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(ToolName="backup_asset"))
class UMCPHandler_BackupAsset : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Full package path of the asset (e.g. /Game/Widgets/WB_Hotkeys)"))
FString AssetPath;
virtual FString GetDescription() const override
{
return TEXT("Copy an asset's .uasset file to a .uasset.bak backup.");
}
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
{
FString Filename = FPaths::ConvertRelativePathToFull(
FPackageName::LongPackageNameToFilename(AssetPath, FPackageName::GetAssetPackageExtension()));
if (!IFileManager::Get().FileExists(*Filename))
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Asset file not found: %s"), *Filename));
}
FString BackupFilename = Filename + TEXT(".bak");
uint32 CopyResult = IFileManager::Get().Copy(*BackupFilename, *Filename, true);
if (CopyResult != COPY_OK)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to back up %s"), *Filename));
}
Result->SetStringField(TEXT("backupFile"), BackupFilename);
}
};
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(ToolName="restore_asset"))
class UMCPHandler_RestoreAsset : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Full package path of the asset (e.g. /Game/Widgets/WB_Hotkeys)"))
FString AssetPath;
virtual FString GetDescription() const override
{
return TEXT("Restore a .uasset file from its .uasset.bak backup, reloading it in the editor.");
}
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
{
FString Filename = FPaths::ConvertRelativePathToFull(
FPackageName::LongPackageNameToFilename(AssetPath, FPackageName::GetAssetPackageExtension()));
FString BackupFilename = Filename + TEXT(".bak");
if (!IFileManager::Get().FileExists(*BackupFilename))
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Backup file not found: %s"), *BackupFilename));
}
// Release file handles if the package is loaded
UPackage* Package = FindPackage(nullptr, *AssetPath);
if (Package)
{
ResetLoaders(Package);
}
// Copy backup over the original
uint32 CopyResult = IFileManager::Get().Copy(*Filename, *BackupFilename, true);
if (CopyResult != COPY_OK)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to restore %s"), *Filename));
}
// Reload the package if it was loaded
if (Package)
{
bool bReloaded = false;
FText ErrorMessage;
UEditorLoadingAndSavingUtils::ReloadPackages({Package}, bReloaded, ErrorMessage, EReloadPackagesInteractionMode::AssumePositive);
}
Result->SetStringField(TEXT("restoredFrom"), BackupFilename);
}
};

View File

@@ -253,23 +253,12 @@ void FMCPServer::DispatchToolCall(const FString& ToolName, const FJsonObject* Pa
{ {
if (UClass** HandlerClass = MCPHandlerRegistry.Find(ToolName)) if (UClass** HandlerClass = MCPHandlerRegistry.Find(ToolName))
{ {
const bool bIsMutation = MutationEndpoints.Contains(ToolName);
if (bIsMutation && GEditor)
{
GEditor->BeginTransaction(FText::FromString(FString::Printf(TEXT("BlueprintMCP: %s"), *ToolName)));
}
TStrongObjectPtr<UObject> HandlerObj(NewObject<UObject>(GetTransientPackage(), *HandlerClass)); TStrongObjectPtr<UObject> HandlerObj(NewObject<UObject>(GetTransientPackage(), *HandlerClass));
IMCPHandler* Handler = Cast<IMCPHandler>(HandlerObj.Get()); IMCPHandler* Handler = Cast<IMCPHandler>(HandlerObj.Get());
if (MCPUtils::PopulateFromJson(HandlerObj->GetClass(), HandlerObj.Get(), Params, Result)) if (MCPUtils::PopulateFromJson(HandlerObj->GetClass(), HandlerObj.Get(), Params, Result))
{ {
Handler->Handle(Params, Result); Handler->Handle(Params, Result);
} }
if (bIsMutation && GEditor)
{
GEditor->EndTransaction();
}
} }
else else
{ {
@@ -316,7 +305,6 @@ bool FMCPServer::Start(int32 InPort, bool bEditorMode)
// Register handlers // Register handlers
BuildMCPHandlerRegistry(); BuildMCPHandlerRegistry();
RegisterHandlers();
// Create TCP listen socket // Create TCP listen socket
ISocketSubsystem* SocketSub = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM); ISocketSubsystem* SocketSub = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM);
@@ -535,65 +523,9 @@ bool FMCPServer::ProcessOneRequest()
} }
// ============================================================ // ============================================================
// RegisterHandlers / BuildMCPHandlerRegistry // BuildMCPHandlerRegistry
// ============================================================ // ============================================================
void FMCPServer::RegisterHandlers()
{
// Mutation endpoints — wrapped in undo transactions by DispatchToolCall()
MutationEndpoints = {
TEXT("replace_function_calls_in_blueprint"),
TEXT("change_blueprint_variable_type"),
TEXT("change_function_parameter_type"),
TEXT("remove_function_parameter"),
TEXT("delete_asset"),
TEXT("connect_blueprint_pins"),
TEXT("disconnect_blueprint_pins"),
TEXT("refresh_all_nodes_in_graph"),
TEXT("set_pin_default_values"),
TEXT("set_node_positions"),
TEXT("change_struct_node_type"),
TEXT("delete_node_from_graph"),
TEXT("duplicate_nodes_in_graph"),
TEXT("spawn_nodes_in_graph"),
TEXT("set_node_comment"),
TEXT("rename_asset"),
TEXT("reparent_blueprint"),
TEXT("set_class_default_value"),
TEXT("create_blueprint_asset"),
TEXT("create_blueprint_graph"),
TEXT("delete_blueprint_graph"),
TEXT("rename_blueprint_graph"),
TEXT("add_blueprint_variable"),
TEXT("remove_blueprint_variable"),
TEXT("set_blueprint_variable_metadata"),
TEXT("add_blueprint_interface"),
TEXT("remove_blueprint_interface"),
TEXT("add_event_dispatcher"),
TEXT("add_function_parameter"),
TEXT("add_blueprint_component"),
TEXT("remove_blueprint_component"),
TEXT("create_material_asset"),
TEXT("set_material_property"),
TEXT("add_material_expression"),
TEXT("delete_material_expression"),
TEXT("connect_material_expression_pins"),
TEXT("disconnect_material_expression_pin"),
TEXT("set_material_expression_property"),
TEXT("set_material_expression_position"),
TEXT("create_material_instance_asset"),
TEXT("set_material_instance_parameter"),
TEXT("reparent_material_instance"),
TEXT("create_material_function_asset"),
TEXT("add_anim_state_to_machine"),
TEXT("remove_anim_state_from_machine"),
TEXT("add_anim_state_transition"),
TEXT("set_anim_transition_rule"),
TEXT("set_anim_state_animation"),
};
}
void FMCPServer::BuildMCPHandlerRegistry() void FMCPServer::BuildMCPHandlerRegistry()
{ {
for (TObjectIterator<UClass> It; It; ++It) for (TObjectIterator<UClass> It; It; ++It)

View File

@@ -51,8 +51,6 @@ public:
private: private:
// ----- Tool dispatch ----- // ----- Tool dispatch -----
TMap<FString, UClass*> MCPHandlerRegistry; // tool name -> UMCPHandler subclass TMap<FString, UClass*> MCPHandlerRegistry; // tool name -> UMCPHandler subclass
TSet<FString> MutationEndpoints;
void RegisterHandlers();
void BuildMCPHandlerRegistry(); void BuildMCPHandlerRegistry();
// Dispatch a tool call to the appropriate handler // Dispatch a tool call to the appropriate handler