From 044ec3ca3f0a5fa8b1e513563e6f76ca65c5ce91 Mon Sep 17 00:00:00 2001 From: nan-chen Date: Mon, 8 Sep 2025 15:26:29 -0700 Subject: [PATCH] added tool/functions for manage_prefab and refactored manage_gameobject.py --- UnityMcpBridge/Editor/MCPForUnityBridge.cs | 1 + .../Editor/Tools/CommandRegistry.cs | 1 + UnityMcpBridge/Editor/Tools/ManageAsset.cs | 7 +- .../Editor/Tools/ManageGameObject.cs | 136 +-- UnityMcpBridge/Editor/Tools/ManagePrefab.cs | 952 ++++++++++++++++++ .../Editor/Tools/ManagePrefab.cs.meta | 2 + UnityMcpBridge/UnityMcpServer~/src/server.py | 3 +- .../UnityMcpServer~/src/tools/__init__.py | 2 + .../src/tools/manage_gameobject.py | 28 +- .../src/tools/manage_prefab.py | 184 ++++ 10 files changed, 1162 insertions(+), 154 deletions(-) create mode 100644 UnityMcpBridge/Editor/Tools/ManagePrefab.cs create mode 100644 UnityMcpBridge/Editor/Tools/ManagePrefab.cs.meta create mode 100644 UnityMcpBridge/UnityMcpServer~/src/tools/manage_prefab.py diff --git a/UnityMcpBridge/Editor/MCPForUnityBridge.cs b/UnityMcpBridge/Editor/MCPForUnityBridge.cs index 0fadce31..2c403aea 100644 --- a/UnityMcpBridge/Editor/MCPForUnityBridge.cs +++ b/UnityMcpBridge/Editor/MCPForUnityBridge.cs @@ -879,6 +879,7 @@ private static string ExecuteCommand(Command command) "manage_gameobject" => ManageGameObject.HandleCommand(paramsObject), "manage_asset" => ManageAsset.HandleCommand(paramsObject), "manage_shader" => ManageShader.HandleCommand(paramsObject), + "manage_prefab" => ManagePrefab.HandleCommand(paramsObject), "read_console" => ReadConsole.HandleCommand(paramsObject), "execute_menu_item" => ExecuteMenuItem.HandleCommand(paramsObject), _ => throw new ArgumentException( diff --git a/UnityMcpBridge/Editor/Tools/CommandRegistry.cs b/UnityMcpBridge/Editor/Tools/CommandRegistry.cs index 55c7425b..a564fae8 100644 --- a/UnityMcpBridge/Editor/Tools/CommandRegistry.cs +++ b/UnityMcpBridge/Editor/Tools/CommandRegistry.cs @@ -21,6 +21,7 @@ public static class CommandRegistry { "HandleReadConsole", ReadConsole.HandleCommand }, { "HandleExecuteMenuItem", ExecuteMenuItem.HandleCommand }, { "HandleManageShader", ManageShader.HandleCommand}, + { "HandleManagePrefab", ManagePrefab.HandleCommand }, }; /// diff --git a/UnityMcpBridge/Editor/Tools/ManageAsset.cs b/UnityMcpBridge/Editor/Tools/ManageAsset.cs index 70e3ff65..137e1dda 100644 --- a/UnityMcpBridge/Editor/Tools/ManageAsset.cs +++ b/UnityMcpBridge/Editor/Tools/ManageAsset.cs @@ -231,14 +231,9 @@ private static object CreateAsset(JObject @params) } else if (lowerAssetType == "prefab") { - // Creating prefabs usually involves saving an existing GameObject hierarchy. - // A common pattern is to create an empty GameObject, configure it, and then save it. return Response.Error( - "Creating prefabs programmatically usually requires a source GameObject. Use manage_gameobject to create/configure, then save as prefab via a separate mechanism or future enhancement." + "Prefab creation should be performed using the 'manage_prefab' tool instead of 'manage_asset'." ); - // Example (conceptual): - // GameObject source = GameObject.Find(properties["sourceGameObject"].ToString()); - // if(source != null) PrefabUtility.SaveAsPrefabAsset(source, fullPath); } // TODO: Add more asset types (Animation Controller, Scene, etc.) else diff --git a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs index c3357ed9..d3d7b793 100644 --- a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs +++ b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs @@ -77,46 +77,12 @@ public static object HandleCommand(JObject @params) if (action == "modify" || action == "set_component_property") { Debug.Log( - $"[ManageGameObject->ManageAsset] Redirecting action '{action}' for prefab '{targetPath}' to ManageAsset." + $"[ManageGameObject->ManagePrefab] Redirecting action '{action}' for prefab '{targetPath}' to ManagePrefab." + ); + // Redirect to ManagePrefab for prefab operations + return Response.Error( + $"Action '{action}' on prefab '{targetPath}' should be performed using the 'manage_prefab' tool instead of 'manage_gameobject'." ); - // Prepare params for ManageAsset.ModifyAsset - JObject assetParams = new JObject(); - assetParams["action"] = "modify"; // ManageAsset uses "modify" - assetParams["path"] = targetPath; - - // Extract properties. - // For 'set_component_property', combine componentName and componentProperties. - // For 'modify', directly use componentProperties. - JObject properties = null; - if (action == "set_component_property") - { - string compName = @params["componentName"]?.ToString(); - JObject compProps = @params["componentProperties"]?[compName] as JObject; // Handle potential nesting - if (string.IsNullOrEmpty(compName)) - return Response.Error( - "Missing 'componentName' for 'set_component_property' on prefab." - ); - if (compProps == null) - return Response.Error( - $"Missing or invalid 'componentProperties' for component '{compName}' for 'set_component_property' on prefab." - ); - - properties = new JObject(); - properties[compName] = compProps; - } - else // action == "modify" - { - properties = @params["componentProperties"] as JObject; - if (properties == null) - return Response.Error( - "Missing 'componentProperties' for 'modify' action on prefab." - ); - } - - assetParams["properties"] = properties; - - // Call ManageAsset handler - return ManageAsset.HandleCommand(assetParams); } else if ( action == "delete" @@ -127,7 +93,7 @@ public static object HandleCommand(JObject @params) { // Explicitly block other modifications on the prefab asset itself via manage_gameobject return Response.Error( - $"Action '{action}' on a prefab asset ('{targetPath}') should be performed using the 'manage_asset' command." + $"Action '{action}' on a prefab asset ('{targetPath}') should be performed using the 'manage_prefab' tool." ); } // Allow 'create' (instantiation) and 'find' to proceed, although finding a prefab asset by path might be less common via manage_gameobject. @@ -183,8 +149,7 @@ private static object CreateGameObject(JObject @params) return Response.Error("'name' parameter is required for 'create' action."); } - // Get prefab creation parameters - bool saveAsPrefab = @params["saveAsPrefab"]?.ToObject() ?? false; + // Check for prefab path parameter (for instantiation only) string prefabPath = @params["prefabPath"]?.ToString(); string tag = @params["tag"]?.ToString(); // Get tag for creation string primitiveType = @params["primitiveType"]?.ToString(); // Keep primitiveType check @@ -478,96 +443,19 @@ private static object CreateGameObject(JObject @params) } } - // Save as Prefab ONLY if we *created* a new object AND saveAsPrefab is true - GameObject finalInstance = newGo; // Use this for selection and return data - if (createdNewObject && saveAsPrefab) - { - string finalPrefabPath = prefabPath; // Use a separate variable for saving path - // This check should now happen *before* attempting to save - if (string.IsNullOrEmpty(finalPrefabPath)) - { - // Clean up the created object before returning error - UnityEngine.Object.DestroyImmediate(newGo); - return Response.Error( - "'prefabPath' is required when 'saveAsPrefab' is true and creating a new object." - ); - } - // Ensure the *saving* path ends with .prefab - if (!finalPrefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)) - { - Debug.Log( - $"[ManageGameObject.Create] Appending .prefab extension to save path: '{finalPrefabPath}' -> '{finalPrefabPath}.prefab'" - ); - finalPrefabPath += ".prefab"; - } - - try - { - // Ensure directory exists using the final saving path - string directoryPath = System.IO.Path.GetDirectoryName(finalPrefabPath); - if ( - !string.IsNullOrEmpty(directoryPath) - && !System.IO.Directory.Exists(directoryPath) - ) - { - System.IO.Directory.CreateDirectory(directoryPath); - AssetDatabase.Refresh(); // Refresh asset database to recognize the new folder - Debug.Log( - $"[ManageGameObject.Create] Created directory for prefab: {directoryPath}" - ); - } - // Use SaveAsPrefabAssetAndConnect with the final saving path - finalInstance = PrefabUtility.SaveAsPrefabAssetAndConnect( - newGo, - finalPrefabPath, - InteractionMode.UserAction - ); - - if (finalInstance == null) - { - // Destroy the original if saving failed somehow (shouldn't usually happen if path is valid) - UnityEngine.Object.DestroyImmediate(newGo); - return Response.Error( - $"Failed to save GameObject '{name}' as prefab at '{finalPrefabPath}'. Check path and permissions." - ); - } - Debug.Log( - $"[ManageGameObject.Create] GameObject '{name}' saved as prefab to '{finalPrefabPath}' and instance connected." - ); - // Mark the new prefab asset as dirty? Not usually necessary, SaveAsPrefabAsset handles it. - // EditorUtility.SetDirty(finalInstance); // Instance is handled by SaveAsPrefabAssetAndConnect - } - catch (Exception e) - { - // Clean up the instance if prefab saving fails - UnityEngine.Object.DestroyImmediate(newGo); // Destroy the original attempt - return Response.Error($"Error saving prefab '{finalPrefabPath}': {e.Message}"); - } - } + GameObject finalInstance = newGo; // Select the instance in the scene (either prefab instance or newly created/saved one) Selection.activeGameObject = finalInstance; - // Determine appropriate success message using the potentially updated or original path - string messagePrefabPath = - finalInstance == null - ? originalPrefabPath - : AssetDatabase.GetAssetPath( - PrefabUtility.GetCorrespondingObjectFromSource(finalInstance) - ?? (UnityEngine.Object)finalInstance - ); + // Determine appropriate success message string successMessage; - if (!createdNewObject && !string.IsNullOrEmpty(messagePrefabPath)) // Instantiated existing prefab - { - successMessage = - $"Prefab '{messagePrefabPath}' instantiated successfully as '{finalInstance.name}'."; - } - else if (createdNewObject && saveAsPrefab && !string.IsNullOrEmpty(messagePrefabPath)) // Created new and saved as prefab + if (!createdNewObject && !string.IsNullOrEmpty(originalPrefabPath)) // Instantiated existing prefab { successMessage = - $"GameObject '{finalInstance.name}' created and saved as prefab to '{messagePrefabPath}'."; + $"Prefab '{originalPrefabPath}' instantiated successfully as '{finalInstance.name}'."; } - else // Created new primitive or empty GO, didn't save as prefab + else // Created new primitive or empty GameObject { successMessage = $"GameObject '{finalInstance.name}' created successfully in scene."; diff --git a/UnityMcpBridge/Editor/Tools/ManagePrefab.cs b/UnityMcpBridge/Editor/Tools/ManagePrefab.cs new file mode 100644 index 00000000..87a15269 --- /dev/null +++ b/UnityMcpBridge/Editor/Tools/ManagePrefab.cs @@ -0,0 +1,952 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.SceneManagement; +using MCPForUnity.Editor.Helpers; + +namespace MCPForUnity.Editor.Tools +{ + /// + /// Handles comprehensive prefab management operations. + /// Consolidates prefab functionality previously scattered across ManageGameObject and ManageAsset. + /// + public static class ManagePrefab + { + /// + /// Main handler for all prefab management commands. + /// + public static object HandleCommand(JObject @params) + { + if (@params == null) + { + return Response.Error("Parameters cannot be null."); + } + + string action = @params["action"]?.ToString()?.ToLower(); + if (string.IsNullOrEmpty(action)) + { + return Response.Error("Action parameter is required."); + } + + try + { + switch (action) + { + case "create": + return CreatePrefab(@params); + case "instantiate": + return InstantiatePrefab(@params); + case "open": + return OpenPrefab(@params); + case "close": + return ClosePrefab(@params); + case "save": + return SavePrefab(@params); + case "modify": + return ModifyPrefab(@params); + case "find": + return FindPrefabs(@params); + case "get_info": + return GetPrefabInfo(@params); + case "variant": + return CreatePrefabVariant(@params); + case "unpack": + return UnpackPrefab(@params); + default: + return Response.Error($"Unknown action: '{action}'. Valid actions are: create, instantiate, open, close, save, modify, find, get_info, variant, unpack."); + } + } + catch (Exception e) + { + Debug.LogError($"[ManagePrefab] Action '{action}' failed: {e}"); + return Response.Error($"Internal error processing action '{action}': {e.Message}"); + } + } + + /// + /// Creates a new prefab from a GameObject or creates an empty prefab. + /// + private static object CreatePrefab(JObject @params) + { + string sourceType = @params["sourceType"]?.ToString()?.ToLower() ?? "gameobject"; + string prefabPath = @params["prefabPath"]?.ToString(); + + if (string.IsNullOrEmpty(prefabPath)) + { + return Response.Error("prefabPath is required for create action."); + } + + // Ensure .prefab extension + if (!prefabPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)) + { + prefabPath += ".prefab"; + } + + // Ensure directory exists + string directory = Path.GetDirectoryName(prefabPath); + if (!string.IsNullOrEmpty(directory)) + { + string fullDirPath = Path.Combine(Application.dataPath.Replace("/Assets", ""), directory); + if (!Directory.Exists(fullDirPath)) + { + Directory.CreateDirectory(fullDirPath); + AssetDatabase.Refresh(); + } + } + + try + { + GameObject prefabAsset = null; + + switch (sourceType) + { + case "gameobject": + prefabAsset = CreateFromGameObject(@params, prefabPath); + break; + case "empty": + prefabAsset = CreateEmptyPrefab(@params, prefabPath); + break; + default: + return Response.Error($"Unknown sourceType: '{sourceType}'. Valid types are: gameobject, empty."); + } + + if (prefabAsset == null) + { + return Response.Error($"Failed to create prefab at '{prefabPath}'."); + } + + return Response.Success($"Prefab created successfully at '{prefabPath}'", new + { + prefabPath = prefabPath, + prefabName = prefabAsset.name, + sourceType = sourceType, + guid = AssetDatabase.AssetPathToGUID(prefabPath) + }); + } + catch (Exception e) + { + Debug.LogError($"[ManagePrefab] Create failed: {e.Message}"); + return Response.Error($"Failed to create prefab: {e.Message}"); + } + } + + /// + /// Creates a prefab from an existing GameObject. + /// + private static GameObject CreateFromGameObject(JObject @params, string prefabPath) + { + string sourceObject = @params["sourceObject"]?.ToString(); + if (string.IsNullOrEmpty(sourceObject)) + { + return Response.Error("sourceObject is required when sourceType is 'gameobject'.") as GameObject; + } + + // Find the source GameObject + GameObject sourceGo = null; + + // Try by instance ID first + if (int.TryParse(sourceObject, out int instanceId)) + { + var allObjects = UnityEngine.Object.FindObjectsOfType(true); + sourceGo = allObjects.FirstOrDefault(go => go.GetInstanceID() == instanceId); + } + + // Try by name if ID search failed + if (sourceGo == null) + { + sourceGo = GameObject.Find(sourceObject); + } + + if (sourceGo == null) + { + throw new ArgumentException($"Source GameObject '{sourceObject}' not found."); + } + + // Create the prefab + GameObject prefabAsset = PrefabUtility.SaveAsPrefabAsset(sourceGo, prefabPath); + + if (prefabAsset != null) + { + Debug.Log($"[ManagePrefab] Created prefab '{prefabPath}' from GameObject '{sourceGo.name}'"); + } + + return prefabAsset; + } + + /// + /// Creates an empty prefab with optional basic components. + /// + private static GameObject CreateEmptyPrefab(JObject @params, string prefabPath) + { + string prefabName = @params["prefabName"]?.ToString() ?? Path.GetFileNameWithoutExtension(prefabPath); + + // Create temporary GameObject + GameObject tempGo = new GameObject(prefabName); + + // Add any requested components + if (@params["components"] is JArray componentsArray) + { + foreach (var component in componentsArray) + { + string componentName = component.ToString(); + Type componentType = GetComponentType(componentName); + if (componentType != null && componentType != typeof(Transform)) + { + tempGo.AddComponent(componentType); + } + } + } + + try + { + // Save as prefab + GameObject prefabAsset = PrefabUtility.SaveAsPrefabAsset(tempGo, prefabPath); + + if (prefabAsset != null) + { + Debug.Log($"[ManagePrefab] Created empty prefab '{prefabPath}'"); + } + + return prefabAsset; + } + finally + { + // Clean up temporary object + UnityEngine.Object.DestroyImmediate(tempGo); + } + } + + /// + /// Instantiates a prefab in the current scene. + /// + private static object InstantiatePrefab(JObject @params) + { + string prefabPath = @params["prefabPath"]?.ToString(); + if (string.IsNullOrEmpty(prefabPath)) + { + return Response.Error("prefabPath is required for instantiate action."); + } + + GameObject prefabAsset = AssetDatabase.LoadAssetAtPath(prefabPath); + if (prefabAsset == null) + { + return Response.Error($"Prefab not found at path: '{prefabPath}'"); + } + + try + { + GameObject instance = PrefabUtility.InstantiatePrefab(prefabAsset) as GameObject; + + if (instance == null) + { + return Response.Error($"Failed to instantiate prefab '{prefabPath}'"); + } + + // Apply position, rotation, scale if provided + ApplyTransformParams(instance, @params); + + // Set parent if provided + string parentName = @params["parent"]?.ToString(); + if (!string.IsNullOrEmpty(parentName)) + { + GameObject parent = GameObject.Find(parentName); + if (parent != null) + { + instance.transform.SetParent(parent.transform); + } + } + + // Register undo + Undo.RegisterCreatedObjectUndo(instance, $"Instantiate Prefab '{prefabAsset.name}'"); + + return Response.Success($"Prefab '{prefabPath}' instantiated successfully", new + { + instanceName = instance.name, + instanceID = instance.GetInstanceID(), + prefabPath = prefabPath, + position = new { x = instance.transform.position.x, y = instance.transform.position.y, z = instance.transform.position.z } + }); + } + catch (Exception e) + { + Debug.LogError($"[ManagePrefab] Instantiate failed: {e.Message}"); + return Response.Error($"Failed to instantiate prefab: {e.Message}"); + } + } + + /// + /// Opens a prefab in Prefab Mode for editing. + /// + private static object OpenPrefab(JObject @params) + { + string prefabPath = @params["prefabPath"]?.ToString(); + if (string.IsNullOrEmpty(prefabPath)) + { + return Response.Error("prefabPath is required for open action."); + } + + // Check if prefab exists + GameObject prefabAsset = AssetDatabase.LoadAssetAtPath(prefabPath); + if (prefabAsset == null) + { + return Response.Error($"Prefab not found at path: '{prefabPath}'"); + } + + try + { + bool inContext = @params["inContext"]?.ToObject() ?? false; + + // Open in Prefab Mode + var stage = PrefabStageUtility.OpenPrefab(prefabPath); + + if (stage == null) + { + return Response.Error($"Failed to open prefab '{prefabPath}' in Prefab Mode"); + } + + return Response.Success($"Prefab '{prefabPath}' opened in Prefab Mode", new + { + prefabPath = prefabPath, + stagePath = stage.assetPath, + prefabName = Path.GetFileNameWithoutExtension(prefabPath), + isInContext = stage.mode == PrefabStage.Mode.InContext + }); + } + catch (Exception e) + { + Debug.LogError($"[ManagePrefab] Open failed: {e.Message}"); + return Response.Error($"Failed to open prefab: {e.Message}"); + } + } + + /// + /// Closes the currently open prefab and returns to main stage. + /// + private static object ClosePrefab(JObject @params) + { + var currentStage = PrefabStageUtility.GetCurrentPrefabStage(); + if (currentStage == null) + { + return Response.Error("No prefab is currently open in Prefab Mode"); + } + + try + { + bool saveChanges = @params["saveChanges"]?.ToObject() ?? true; + string prefabPath = currentStage.assetPath; + bool hadUnsavedChanges = currentStage.scene.isDirty; + + if (saveChanges && hadUnsavedChanges) + { + // Save changes before closing + EditorSceneManager.SaveScene(currentStage.scene); + } + + // Return to main stage + StageUtility.GoToMainStage(); + + return Response.Success($"Closed prefab: '{prefabPath}'", new + { + prefabPath = prefabPath, + savedChanges = saveChanges && hadUnsavedChanges, + hadUnsavedChanges = hadUnsavedChanges + }); + } + catch (Exception e) + { + Debug.LogError($"[ManagePrefab] Close failed: {e.Message}"); + return Response.Error($"Failed to close prefab: {e.Message}"); + } + } + + /// + /// Saves the currently open prefab. + /// + private static object SavePrefab(JObject @params) + { + var currentStage = PrefabStageUtility.GetCurrentPrefabStage(); + if (currentStage == null) + { + return Response.Error("No prefab is currently open in Prefab Mode"); + } + + try + { + string prefabPath = currentStage.assetPath; + + if (!currentStage.scene.isDirty) + { + return Response.Success($"Prefab '{prefabPath}' has no unsaved changes", new + { + prefabPath = prefabPath, + wasDirty = false + }); + } + + // Save the prefab + EditorSceneManager.SaveScene(currentStage.scene); + + return Response.Success($"Prefab '{prefabPath}' saved successfully", new + { + prefabPath = prefabPath, + wasDirty = true + }); + } + catch (Exception e) + { + Debug.LogError($"[ManagePrefab] Save failed: {e.Message}"); + return Response.Error($"Failed to save prefab: {e.Message}"); + } + } + + /// + /// Finds prefabs based on search criteria. + /// + private static object FindPrefabs(JObject @params) + { + string searchTerm = @params["searchTerm"]?.ToString() ?? ""; + string searchType = @params["searchType"]?.ToString()?.ToLower() ?? "name"; + bool includeVariants = @params["includeVariants"]?.ToObject() ?? true; + + try + { + string searchFilter = string.IsNullOrEmpty(searchTerm) + ? "t:Prefab" + : $"t:Prefab {searchTerm}"; + + string[] guids = AssetDatabase.FindAssets(searchFilter); + var results = new List(); + + foreach (string guid in guids) + { + string path = AssetDatabase.GUIDToAssetPath(guid); + GameObject prefab = AssetDatabase.LoadAssetAtPath(path); + + if (prefab != null) + { + var prefabType = PrefabUtility.GetPrefabAssetType(prefab); + bool isVariant = prefabType == PrefabAssetType.Variant; + + if (!includeVariants && isVariant) continue; + + results.Add(new + { + name = prefab.name, + path = path, + guid = guid, + isVariant = isVariant, + prefabType = prefabType.ToString(), + fileSize = new FileInfo(path).Length + }); + } + } + + return Response.Success($"Found {results.Count} prefabs", results); + } + catch (Exception e) + { + Debug.LogError($"[ManagePrefab] Find failed: {e.Message}"); + return Response.Error($"Failed to find prefabs: {e.Message}"); + } + } + + /// + /// Gets detailed information about a specific prefab. + /// + private static object GetPrefabInfo(JObject @params) + { + string prefabPath = @params["prefabPath"]?.ToString(); + if (string.IsNullOrEmpty(prefabPath)) + { + return Response.Error("prefabPath is required for get_info action."); + } + + GameObject prefabAsset = AssetDatabase.LoadAssetAtPath(prefabPath); + if (prefabAsset == null) + { + return Response.Error($"Prefab not found at path: '{prefabPath}'"); + } + + try + { + var prefabType = PrefabUtility.GetPrefabAssetType(prefabAsset); + var components = prefabAsset.GetComponents(); + + var info = new + { + name = prefabAsset.name, + path = prefabPath, + guid = AssetDatabase.AssetPathToGUID(prefabPath), + prefabType = prefabType.ToString(), + isVariant = prefabType == PrefabAssetType.Variant, + componentCount = components.Length, + components = components.Where(c => c != null).Select(c => c.GetType().Name).ToArray(), + fileSize = new FileInfo(prefabPath).Length, + lastModified = File.GetLastWriteTime(prefabPath).ToString("yyyy-MM-dd HH:mm:ss") + }; + + return Response.Success($"Retrieved info for prefab '{prefabPath}'", info); + } + catch (Exception e) + { + Debug.LogError($"[ManagePrefab] GetInfo failed: {e.Message}"); + return Response.Error($"Failed to get prefab info: {e.Message}"); + } + } + + /// + /// Creates a prefab variant from an existing prefab. + /// Note: Creates a copy of the prefab rather than a true variant due to Unity API compatibility. + /// + private static object CreatePrefabVariant(JObject @params) + { + string basePrefabPath = @params["basePrefabPath"]?.ToString(); + string variantPath = @params["variantPath"]?.ToString(); + + if (string.IsNullOrEmpty(basePrefabPath)) + { + return Response.Error("basePrefabPath is required for variant action."); + } + + if (string.IsNullOrEmpty(variantPath)) + { + return Response.Error("variantPath is required for variant action."); + } + + GameObject basePrefab = AssetDatabase.LoadAssetAtPath(basePrefabPath); + if (basePrefab == null) + { + return Response.Error($"Base prefab not found at path: '{basePrefabPath}'"); + } + + try + { + // Ensure .prefab extension + if (!variantPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)) + { + variantPath += ".prefab"; + } + + // Create a copy of the prefab (simpler approach for compatibility) + bool copySuccess = AssetDatabase.CopyAsset(basePrefabPath, variantPath); + if (!copySuccess) + { + return Response.Error($"Failed to copy prefab from '{basePrefabPath}' to '{variantPath}'"); + } + + // Load the copied prefab + GameObject variant = AssetDatabase.LoadAssetAtPath(variantPath); + if (variant == null) + { + return Response.Error($"Failed to load copied prefab at '{variantPath}'"); + } + + return Response.Success($"Prefab copy created at '{variantPath}' (Note: This is a copy, not a true variant)", new + { + variantPath = variantPath, + basePrefabPath = basePrefabPath, + variantName = variant.name, + guid = AssetDatabase.AssetPathToGUID(variantPath), + isTrueVariant = false // Indicate this is a copy, not a true variant + }); + } + catch (Exception e) + { + Debug.LogError($"[ManagePrefab] CreateVariant failed: {e.Message}"); + return Response.Error($"Failed to create prefab variant: {e.Message}"); + } + } + + /// + /// Unpacks a prefab instance to regular GameObjects. + /// + private static object UnpackPrefab(JObject @params) + { + string targetObject = @params["targetObject"]?.ToString(); + if (string.IsNullOrEmpty(targetObject)) + { + return Response.Error("targetObject is required for unpack action."); + } + + // Find the target GameObject + GameObject target = FindGameObjectByNameOrId(targetObject); + if (target == null) + { + return Response.Error($"Target GameObject '{targetObject}' not found."); + } + + // Check if it's a prefab instance + if (PrefabUtility.GetPrefabInstanceStatus(target) == PrefabInstanceStatus.NotAPrefab) + { + return Response.Error($"GameObject '{target.name}' is not a prefab instance."); + } + + try + { + bool unpackCompletely = @params["unpackCompletely"]?.ToObject() ?? false; + + if (unpackCompletely) + { + PrefabUtility.UnpackPrefabInstance(target, PrefabUnpackMode.Completely, InteractionMode.UserAction); + } + else + { + PrefabUtility.UnpackPrefabInstance(target, PrefabUnpackMode.OutermostRoot, InteractionMode.UserAction); + } + + return Response.Success($"Prefab instance '{target.name}' unpacked successfully", new + { + targetName = target.name, + instanceID = target.GetInstanceID(), + unpackedCompletely = unpackCompletely + }); + } + catch (Exception e) + { + Debug.LogError($"[ManagePrefab] Unpack failed: {e.Message}"); + return Response.Error($"Failed to unpack prefab instance: {e.Message}"); + } + } + + /// + /// Modifies objects within a prefab by opening it, making changes, and optionally saving. + /// + private static object ModifyPrefab(JObject @params) + { + string prefabPath = @params["prefabPath"]?.ToString(); + if (string.IsNullOrEmpty(prefabPath)) + { + return Response.Error("prefabPath is required for modify action."); + } + + string modifyTarget = @params["modifyTarget"]?.ToString(); + if (string.IsNullOrEmpty(modifyTarget)) + { + return Response.Error("modifyTarget is required for modify action. Specify the name of the object to modify within the prefab."); + } + + GameObject prefabAsset = AssetDatabase.LoadAssetAtPath(prefabPath); + if (prefabAsset == null) + { + return Response.Error($"Prefab not found at path: '{prefabPath}'"); + } + + try + { + // Clear any existing selection first to prevent inspector conflicts + Selection.activeObject = null; + + bool autoSave = @params["autoSave"]?.ToObject() ?? true; + bool wasInPrefabMode = PrefabStageUtility.GetCurrentPrefabStage() != null; + string originalPrefabPath = null; + + // If we're already in prefab mode, note the current prefab + if (wasInPrefabMode) + { + originalPrefabPath = PrefabStageUtility.GetCurrentPrefabStage().assetPath; + } + + // Open the target prefab if not already open, or if it's a different prefab + PrefabStage stage = null; + if (!wasInPrefabMode || originalPrefabPath != prefabPath) + { + stage = PrefabStageUtility.OpenPrefab(prefabPath); + if (stage == null) + { + return Response.Error($"Failed to open prefab '{prefabPath}' in Prefab Mode"); + } + } + else + { + stage = PrefabStageUtility.GetCurrentPrefabStage(); + } + + // Find the target object within the prefab + GameObject targetObject = FindObjectInPrefabStage(stage, modifyTarget); + if (targetObject == null) + { + return Response.Error($"Object '{modifyTarget}' not found in prefab '{prefabPath}'. Available objects: {GetObjectNamesInPrefabStage(stage)}"); + } + + // Clear any current selection to prevent Inspector issues + Selection.activeObject = null; + + // Record undo for the modification - be more careful with what we record + Undo.RecordObject(targetObject.transform, $"Modify Prefab Object '{modifyTarget}'"); + + // Only record the GameObject itself if we're going to modify non-transform properties + bool willModifyGameObject = @params["modifyActive"] != null || + @params["modifyName"] != null || + @params["modifyTag"] != null || + @params["modifyLayer"] != null; + + if (willModifyGameObject) + { + Undo.RecordObject(targetObject, $"Modify Prefab Object '{modifyTarget}'"); + } + + // Apply modifications + var modifications = new List(); + + // Transform modifications + if (@params["modifyPosition"] is JArray posArray && posArray.Count >= 3) + { + Vector3 newPos = new Vector3( + posArray[0].ToObject(), + posArray[1].ToObject(), + posArray[2].ToObject() + ); + targetObject.transform.localPosition = newPos; + modifications.Add($"position to {newPos}"); + } + + if (@params["modifyRotation"] is JArray rotArray && rotArray.Count >= 3) + { + Vector3 newRot = new Vector3( + rotArray[0].ToObject(), + rotArray[1].ToObject(), + rotArray[2].ToObject() + ); + targetObject.transform.localEulerAngles = newRot; + modifications.Add($"rotation to {newRot}"); + } + + if (@params["modifyScale"] is JArray scaleArray && scaleArray.Count >= 3) + { + Vector3 newScale = new Vector3( + scaleArray[0].ToObject(), + scaleArray[1].ToObject(), + scaleArray[2].ToObject() + ); + targetObject.transform.localScale = newScale; + modifications.Add($"scale to {newScale}"); + } + + // GameObject property modifications + if (@params["modifyActive"] is JToken activeToken) + { + bool newActive = activeToken.ToObject(); + targetObject.SetActive(newActive); + modifications.Add($"active to {newActive}"); + } + + if (@params["modifyName"] is JToken nameToken) + { + string newName = nameToken.ToString(); + targetObject.name = newName; + modifications.Add($"name to '{newName}'"); + } + + if (@params["modifyTag"] is JToken tagToken) + { + string newTag = tagToken.ToString(); + targetObject.tag = newTag; + modifications.Add($"tag to '{newTag}'"); + } + + if (@params["modifyLayer"] is JToken layerToken) + { + string layerName = layerToken.ToString(); + int layer = LayerMask.NameToLayer(layerName); + if (layer != -1) + { + targetObject.layer = layer; + modifications.Add($"layer to '{layerName}'"); + } + else + { + Debug.LogWarning($"[ManagePrefab] Layer '{layerName}' not found"); + } + } + + // Ensure modifications are applied immediately + EditorUtility.SetDirty(targetObject); + + // Mark the scene as dirty to enable saving + EditorSceneManager.MarkSceneDirty(stage.scene); + + // Auto-save if requested + if (autoSave) + { + try + { + EditorSceneManager.SaveScene(stage.scene); + modifications.Add("auto-saved"); + } + catch (Exception saveEx) + { + Debug.LogWarning($"[ManagePrefab] Auto-save failed: {saveEx.Message}"); + modifications.Add("save-failed"); + } + } + + // Clear selection again to prevent lingering references + Selection.activeObject = null; + + // Force a repaint to update the editor + EditorApplication.QueuePlayerLoopUpdate(); + SceneView.RepaintAll(); + + var result = new + { + prefabPath = prefabPath, + targetObject = modifyTarget, + modifications = modifications, + saved = autoSave && !modifications.Contains("save-failed"), + stageKeptOpen = true + }; + + Debug.Log($"[ManagePrefab] Modified '{modifyTarget}' in prefab '{prefabPath}': {string.Join(", ", modifications)}"); + + return Response.Success($"Successfully modified '{modifyTarget}' in prefab '{prefabPath}'", result); + } + catch (Exception e) + { + // Clear selection on error to prevent lingering inspector issues + try { Selection.activeObject = null; } catch { } + + Debug.LogError($"[ManagePrefab] Modify failed: {e.Message}\n{e.StackTrace}"); + + // Provide more specific error messages for common issues + string errorMessage = e.Message; + if (e is NullReferenceException) + { + errorMessage = "Unity Editor state conflict. Try closing and reopening the prefab, or restart Unity Editor."; + } + else if (e.Message.Contains("Inspector")) + { + errorMessage = "Unity Inspector conflict. The modification was attempted but may have caused editor issues."; + } + + return Response.Error($"Failed to modify prefab: {errorMessage}"); + } + } + + // --- Helper Methods --- + + private static void ApplyTransformParams(GameObject obj, JObject @params) + { + if (@params["position"] is JArray posArray && posArray.Count >= 3) + { + obj.transform.position = new Vector3( + posArray[0].ToObject(), + posArray[1].ToObject(), + posArray[2].ToObject() + ); + } + + if (@params["rotation"] is JArray rotArray && rotArray.Count >= 3) + { + obj.transform.eulerAngles = new Vector3( + rotArray[0].ToObject(), + rotArray[1].ToObject(), + rotArray[2].ToObject() + ); + } + + if (@params["scale"] is JArray scaleArray && scaleArray.Count >= 3) + { + obj.transform.localScale = new Vector3( + scaleArray[0].ToObject(), + scaleArray[1].ToObject(), + scaleArray[2].ToObject() + ); + } + } + + private static GameObject FindGameObjectByNameOrId(string nameOrId) + { + // Try by instance ID first + if (int.TryParse(nameOrId, out int instanceId)) + { + var allObjects = UnityEngine.Object.FindObjectsOfType(true); + var obj = allObjects.FirstOrDefault(go => go.GetInstanceID() == instanceId); + if (obj != null) return obj; + } + + // Try by name + return GameObject.Find(nameOrId); + } + + private static Type GetComponentType(string componentName) + { + // Try common Unity types first + var unityTypes = new[] + { + typeof(Rigidbody), typeof(Collider), typeof(Renderer), typeof(Light), + typeof(Camera), typeof(AudioSource), typeof(Animation), typeof(Animator) + }; + + foreach (var type in unityTypes) + { + if (type.Name.Equals(componentName, StringComparison.OrdinalIgnoreCase)) + return type; + } + + // Try to find in all loaded assemblies + foreach (var assembly in System.AppDomain.CurrentDomain.GetAssemblies()) + { + var type = assembly.GetTypes().FirstOrDefault(t => + typeof(Component).IsAssignableFrom(t) && + t.Name.Equals(componentName, StringComparison.OrdinalIgnoreCase)); + if (type != null) return type; + } + + return null; + } + + /// + /// Finds an object by name within a prefab stage. + /// + private static GameObject FindObjectInPrefabStage(PrefabStage stage, string objectName) + { + if (stage == null || string.IsNullOrEmpty(objectName)) + return null; + + // Search in the prefab root and all its children + Transform[] allTransforms = stage.prefabContentsRoot.GetComponentsInChildren(true); + + foreach (Transform t in allTransforms) + { + if (t.name.Equals(objectName, StringComparison.OrdinalIgnoreCase)) + { + return t.gameObject; + } + } + + return null; + } + + /// + /// Gets a list of all object names in a prefab stage for error reporting. + /// + private static string GetObjectNamesInPrefabStage(PrefabStage stage) + { + if (stage == null) + return "No prefab stage available"; + + try + { + Transform[] allTransforms = stage.prefabContentsRoot.GetComponentsInChildren(true); + var names = allTransforms.Select(t => t.name).Where(n => !string.IsNullOrEmpty(n)).Distinct().Take(10); + string result = string.Join(", ", names); + + if (allTransforms.Length > 10) + { + result += $" (and {allTransforms.Length - 10} more)"; + } + + return result; + } + catch (Exception e) + { + return $"Error listing objects: {e.Message}"; + } + } + } +} \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Tools/ManagePrefab.cs.meta b/UnityMcpBridge/Editor/Tools/ManagePrefab.cs.meta new file mode 100644 index 00000000..69820003 --- /dev/null +++ b/UnityMcpBridge/Editor/Tools/ManagePrefab.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 00dc59525df8c4daeb44f5b27b13ae62 \ No newline at end of file diff --git a/UnityMcpBridge/UnityMcpServer~/src/server.py b/UnityMcpBridge/UnityMcpServer~/src/server.py index 29c7b6a7..5f6f13bd 100644 --- a/UnityMcpBridge/UnityMcpServer~/src/server.py +++ b/UnityMcpBridge/UnityMcpServer~/src/server.py @@ -61,7 +61,8 @@ def asset_creation_strategy() -> str: "- `manage_scene`: Manages scenes.\\n" "- `manage_gameobject`: Manages GameObjects in the scene.\\n" "- `manage_script`: Manages C# script files.\\n" - "- `manage_asset`: Manages prefabs and assets.\\n" + "- `manage_asset`: Manages general assets (materials, textures, scriptable objects).\\n" + "- `manage_prefab`: Comprehensive prefab management (create, instantiate, open, modify, find).\\n" "- `manage_shader`: Manages shaders.\\n\\n" "Tips:\\n" "- Create prefabs for reusable GameObjects.\\n" diff --git a/UnityMcpBridge/UnityMcpServer~/src/tools/__init__.py b/UnityMcpBridge/UnityMcpServer~/src/tools/__init__.py index 43b53096..2336f41d 100644 --- a/UnityMcpBridge/UnityMcpServer~/src/tools/__init__.py +++ b/UnityMcpBridge/UnityMcpServer~/src/tools/__init__.py @@ -6,6 +6,7 @@ from .manage_gameobject import register_manage_gameobject_tools from .manage_asset import register_manage_asset_tools from .manage_shader import register_manage_shader_tools +from .manage_prefab import register_manage_prefab_tools from .read_console import register_read_console_tools from .execute_menu_item import register_execute_menu_item_tools from .resource_tools import register_resource_tools @@ -23,6 +24,7 @@ def register_all_tools(mcp): register_manage_gameobject_tools(mcp) register_manage_asset_tools(mcp) register_manage_shader_tools(mcp) + register_manage_prefab_tools(mcp) register_read_console_tools(mcp) register_execute_menu_item_tools(mcp) # Expose resource wrappers as normal tools so IDEs without resources primitive can use them diff --git a/UnityMcpBridge/UnityMcpServer~/src/tools/manage_gameobject.py b/UnityMcpBridge/UnityMcpServer~/src/tools/manage_gameobject.py index cbe29a31..aa5ea4da 100644 --- a/UnityMcpBridge/UnityMcpServer~/src/tools/manage_gameobject.py +++ b/UnityMcpBridge/UnityMcpServer~/src/tools/manage_gameobject.py @@ -22,9 +22,6 @@ def manage_gameobject( scale: List[float] = None, components_to_add: List[str] = None, # List of component names to add primitive_type: str = None, - save_as_prefab: bool = False, - prefab_path: str = None, - prefab_folder: str = "Assets/Prefabs", # --- Parameters for 'modify' --- set_active: bool = None, layer: str = None, # Layer name @@ -39,7 +36,11 @@ def manage_gameobject( component_name: str = None, includeNonPublicSerialized: bool = None, # Controls serialization of private [SerializeField] fields ) -> Dict[str, Any]: - """Manages GameObjects: create, modify, delete, find, and component operations. + """ + Manages GameObjects in Unity scenes. + + NOTE: For prefab operations (create, modify, open, save prefabs), use the 'manage_prefab' tool instead. + This tool focuses on scene GameObjects and can instantiate existing prefabs into scenes. Args: action: Operation (e.g., 'create', 'modify', 'find', 'add_component', 'remove_component', 'set_component_property', 'get_components'). @@ -92,9 +93,6 @@ def manage_gameobject( "scale": scale, "componentsToAdd": components_to_add, "primitiveType": primitive_type, - "saveAsPrefab": save_as_prefab, - "prefabPath": prefab_path, - "prefabFolder": prefab_folder, "setActive": set_active, "layer": layer, "componentsToRemove": components_to_remove, @@ -108,22 +106,6 @@ def manage_gameobject( } params = {k: v for k, v in params.items() if v is not None} - # --- Handle Prefab Path Logic --- - if action == "create" and params.get("saveAsPrefab"): # Check if 'saveAsPrefab' is explicitly True in params - if "prefabPath" not in params: - if "name" not in params or not params["name"]: - return {"success": False, "message": "Cannot create default prefab path: 'name' parameter is missing."} - # Use the provided prefab_folder (which has a default) and the name to construct the path - constructed_path = f"{prefab_folder}/{params['name']}.prefab" - # Ensure clean path separators (Unity prefers '/') - params["prefabPath"] = constructed_path.replace("\\", "/") - elif not params["prefabPath"].lower().endswith(".prefab"): - return {"success": False, "message": f"Invalid prefab_path: '{params['prefabPath']}' must end with .prefab"} - # Ensure prefab_folder itself isn't sent if prefabPath was constructed or provided - # The C# side only needs the final prefabPath - params.pop("prefab_folder", None) - # -------------------------------- - # Use centralized retry helper response = send_command_with_retry("manage_gameobject", params) diff --git a/UnityMcpBridge/UnityMcpServer~/src/tools/manage_prefab.py b/UnityMcpBridge/UnityMcpServer~/src/tools/manage_prefab.py new file mode 100644 index 00000000..544b9c8d --- /dev/null +++ b/UnityMcpBridge/UnityMcpServer~/src/tools/manage_prefab.py @@ -0,0 +1,184 @@ +""" +Defines the manage_prefab tool for comprehensive Unity prefab management. +""" +from typing import Dict, Any, List, Optional, Literal, Union +from mcp.server.fastmcp import FastMCP, Context +from unity_connection import send_command_with_retry +import json + +def register_manage_prefab_tools(mcp: FastMCP): + """Registers the manage_prefab tool with the MCP server.""" + print("[DEBUG] Starting manage_prefab tool registration...") + + @mcp.tool() + async def manage_prefab( + ctx: Context, + action: Literal["create", "instantiate", "open", "close", "save", "modify", "find", "get_info", "variant", "unpack"], + prefab_path: Optional[str] = None, + source_object: Optional[str] = None, + source_type: Literal["gameobject", "empty"] = "gameobject", + prefab_name: Optional[str] = None, + components: Optional[List[str]] = None, + position: Optional[List[float]] = None, + rotation: Optional[List[float]] = None, + scale: Optional[List[float]] = None, + parent: Optional[str] = None, + in_context: bool = False, + save_changes: bool = True, + search_term: Optional[str] = None, + search_type: Literal["name", "path"] = "name", + include_variants: bool = True, + base_prefab_path: Optional[str] = None, + variant_path: Optional[str] = None, + target_object: Optional[str] = None, + unpack_completely: bool = False, + # New parameters for modify action + modify_target: Optional[str] = None, + modify_position: Optional[List[float]] = None, + modify_rotation: Optional[List[float]] = None, + modify_scale: Optional[List[float]] = None, + modify_active: Optional[bool] = None, + modify_name: Optional[str] = None, + modify_tag: Optional[str] = None, + modify_layer: Optional[str] = None, + auto_save: bool = True + ) -> Dict[str, Any]: + """ + Manages Unity prefabs with comprehensive operations. + + This tool consolidates all prefab-related functionality that was previously + scattered across manage_gameobject and manage_asset tools. + + Args: + action: The prefab operation to perform + prefab_path: Path to the prefab asset (e.g., "Assets/Prefabs/MyPrefab.prefab") + source_object: GameObject name or instanceID to create prefab from (for create action) + source_type: Type of prefab to create ("gameobject" from existing object, "empty" new) + prefab_name: Name for the prefab (used with empty prefabs) + components: List of component names to add to empty prefabs + position: [x, y, z] position for instantiated prefabs + rotation: [x, y, z] rotation for instantiated prefabs + scale: [x, y, z] scale for instantiated prefabs + parent: Parent GameObject name for instantiated prefabs + in_context: Open prefab in context mode (for open action) + save_changes: Whether to save changes when closing prefab + search_term: Term to search for (for find action) + search_type: How to search ("name" or "path") + include_variants: Include prefab variants in search results + base_prefab_path: Base prefab for creating variants + variant_path: Path for the new variant + target_object: GameObject to unpack (for unpack action) + unpack_completely: Unpack completely vs just outermost root + + Returns: + Success/error response with operation results + + Examples: + # Create prefab from existing GameObject + manage_prefab(action="create", source_object="MyGameObject", prefab_path="Assets/Prefabs/MyPrefab.prefab") + + # Create empty prefab with components + manage_prefab(action="create", source_type="empty", prefab_name="EmptyPrefab", + prefab_path="Assets/Prefabs/Empty.prefab", components=["Rigidbody", "Collider"]) + + # Instantiate prefab + manage_prefab(action="instantiate", prefab_path="Assets/Prefabs/MyPrefab.prefab", + position=[0, 5, 0], parent="Container") + + # Open prefab for editing + manage_prefab(action="open", prefab_path="Assets/Prefabs/MyPrefab.prefab") + + # Close prefab and save changes + manage_prefab(action="close", save_changes=True) + + # Find prefabs by name + manage_prefab(action="find", search_term="Player", include_variants=False) + + # Get prefab information + manage_prefab(action="get_info", prefab_path="Assets/Prefabs/MyPrefab.prefab") + + # Create prefab variant + manage_prefab(action="variant", base_prefab_path="Assets/Prefabs/Base.prefab", + variant_path="Assets/Prefabs/Variant.prefab") + + # Unpack prefab instance + manage_prefab(action="unpack", target_object="MyPrefabInstance", unpack_completely=True) + + # Modify objects inside a prefab (opens, modifies, saves automatically) + manage_prefab(action="modify", prefab_path="Assets/Prefabs/MyPrefab.prefab", + modify_target="PipeTop", modify_scale=[0.5, 0.5, 0.5]) + + # Modify multiple properties at once + manage_prefab(action="modify", prefab_path="Assets/Prefabs/Player.prefab", + modify_target="PlayerModel", modify_scale=[1.2, 1.2, 1.2], + modify_position=[0, 0.5, 0], auto_save=True) + """ + + # Build parameters dictionary for Unity + params_dict = { + "action": action, + } + + # Add optional parameters based on action + if prefab_path is not None: + params_dict["prefabPath"] = prefab_path + if source_object is not None: + params_dict["sourceObject"] = source_object + if source_type != "gameobject": + params_dict["sourceType"] = source_type + if prefab_name is not None: + params_dict["prefabName"] = prefab_name + if components is not None: + params_dict["components"] = components + if position is not None: + params_dict["position"] = position + if rotation is not None: + params_dict["rotation"] = rotation + if scale is not None: + params_dict["scale"] = scale + if parent is not None: + params_dict["parent"] = parent + if in_context: + params_dict["inContext"] = in_context + if not save_changes: + params_dict["saveChanges"] = save_changes + if search_term is not None: + params_dict["searchTerm"] = search_term + if search_type != "name": + params_dict["searchType"] = search_type + if not include_variants: + params_dict["includeVariants"] = include_variants + if base_prefab_path is not None: + params_dict["basePrefabPath"] = base_prefab_path + if variant_path is not None: + params_dict["variantPath"] = variant_path + if target_object is not None: + params_dict["targetObject"] = target_object + if unpack_completely: + params_dict["unpackCompletely"] = unpack_completely + + # Add modify-specific parameters + if modify_target is not None: + params_dict["modifyTarget"] = modify_target + if modify_position is not None: + params_dict["modifyPosition"] = modify_position + if modify_rotation is not None: + params_dict["modifyRotation"] = modify_rotation + if modify_scale is not None: + params_dict["modifyScale"] = modify_scale + if modify_active is not None: + params_dict["modifyActive"] = modify_active + if modify_name is not None: + params_dict["modifyName"] = modify_name + if modify_tag is not None: + params_dict["modifyTag"] = modify_tag + if modify_layer is not None: + params_dict["modifyLayer"] = modify_layer + if not auto_save: + params_dict["autoSave"] = auto_save + + # Send command to Unity + resp = send_command_with_retry("manage_prefab", params_dict) + return resp if isinstance(resp, dict) else {"success": False, "message": str(resp)} + + print("[DEBUG] manage_prefab tool registered successfully!") \ No newline at end of file