feat: replace prefab stage actions with headless modify_contents (#635)
Removes stage-based prefab editing (open_stage, close_stage, save_open_stage) in favor of headless modify_contents action that uses PrefabUtility.LoadPrefabContents for reliable automated workflows without UI dialogs. Changes: - Remove open_stage, close_stage, save_open_stage actions - Add modify_contents action for headless prefab editing - Support targeting objects by name or path (e.g., "Turret/Barrel") - Support transform, tag, layer, setActive, name, parent, components operations - Skip saving when no modifications made (avoids unnecessary asset writes) - Delete PrefabStage.cs resource (no longer needed) - Update Python tool description to remove "stages" reference - Consolidate tests from 29 to 14 (covers complex prefabs, reparenting, hierarchy loop guard) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>main
parent
f5ca9173b2
commit
17eb171e31
|
|
@ -1,42 +0,0 @@
|
||||||
using System;
|
|
||||||
using MCPForUnity.Editor.Helpers;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using UnityEditor.SceneManagement;
|
|
||||||
|
|
||||||
namespace MCPForUnity.Editor.Resources.Editor
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Provides information about the current prefab editing context.
|
|
||||||
/// </summary>
|
|
||||||
[McpForUnityResource("get_prefab_stage")]
|
|
||||||
public static class PrefabStage
|
|
||||||
{
|
|
||||||
public static object HandleCommand(JObject @params)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var stage = PrefabStageUtility.GetCurrentPrefabStage();
|
|
||||||
|
|
||||||
if (stage == null)
|
|
||||||
{
|
|
||||||
return new SuccessResponse("No prefab stage is currently open.", new { isOpen = false });
|
|
||||||
}
|
|
||||||
|
|
||||||
var stageInfo = new
|
|
||||||
{
|
|
||||||
isOpen = true,
|
|
||||||
assetPath = stage.assetPath,
|
|
||||||
prefabRootName = stage.prefabContentsRoot?.name,
|
|
||||||
mode = stage.mode.ToString(),
|
|
||||||
isDirty = stage.scene.isDirty
|
|
||||||
};
|
|
||||||
|
|
||||||
return new SuccessResponse("Prefab stage info retrieved.", stageInfo);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
return new ErrorResponse($"Error getting prefab stage info: {e.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 7a30b083e68bd4ae3b3d1ce5a45a9414
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -12,18 +12,17 @@ namespace MCPForUnity.Editor.Tools.Prefabs
|
||||||
{
|
{
|
||||||
[McpForUnityTool("manage_prefabs", AutoRegister = false)]
|
[McpForUnityTool("manage_prefabs", AutoRegister = false)]
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tool to manage Unity Prefab stages and create prefabs from GameObjects.
|
/// Tool to manage Unity Prefabs: create, inspect, and modify prefab assets.
|
||||||
|
/// Uses headless editing (no UI, no dialogs) for reliable automated workflows.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class ManagePrefabs
|
public static class ManagePrefabs
|
||||||
{
|
{
|
||||||
// Action constants
|
// Action constants
|
||||||
private const string ACTION_OPEN_STAGE = "open_stage";
|
|
||||||
private const string ACTION_CLOSE_STAGE = "close_stage";
|
|
||||||
private const string ACTION_SAVE_OPEN_STAGE = "save_open_stage";
|
|
||||||
private const string ACTION_CREATE_FROM_GAMEOBJECT = "create_from_gameobject";
|
private const string ACTION_CREATE_FROM_GAMEOBJECT = "create_from_gameobject";
|
||||||
private const string ACTION_GET_INFO = "get_info";
|
private const string ACTION_GET_INFO = "get_info";
|
||||||
private const string ACTION_GET_HIERARCHY = "get_hierarchy";
|
private const string ACTION_GET_HIERARCHY = "get_hierarchy";
|
||||||
private const string SupportedActions = ACTION_OPEN_STAGE + ", " + ACTION_CLOSE_STAGE + ", " + ACTION_SAVE_OPEN_STAGE + ", " + ACTION_CREATE_FROM_GAMEOBJECT + ", " + ACTION_GET_INFO + ", " + ACTION_GET_HIERARCHY;
|
private const string ACTION_MODIFY_CONTENTS = "modify_contents";
|
||||||
|
private const string SupportedActions = ACTION_CREATE_FROM_GAMEOBJECT + ", " + ACTION_GET_INFO + ", " + ACTION_GET_HIERARCHY + ", " + ACTION_MODIFY_CONTENTS;
|
||||||
|
|
||||||
public static object HandleCommand(JObject @params)
|
public static object HandleCommand(JObject @params)
|
||||||
{
|
{
|
||||||
|
|
@ -42,18 +41,14 @@ namespace MCPForUnity.Editor.Tools.Prefabs
|
||||||
{
|
{
|
||||||
switch (action)
|
switch (action)
|
||||||
{
|
{
|
||||||
case ACTION_OPEN_STAGE:
|
|
||||||
return OpenStage(@params);
|
|
||||||
case ACTION_CLOSE_STAGE:
|
|
||||||
return CloseStage(@params);
|
|
||||||
case ACTION_SAVE_OPEN_STAGE:
|
|
||||||
return SaveOpenStage(@params);
|
|
||||||
case ACTION_CREATE_FROM_GAMEOBJECT:
|
case ACTION_CREATE_FROM_GAMEOBJECT:
|
||||||
return CreatePrefabFromGameObject(@params);
|
return CreatePrefabFromGameObject(@params);
|
||||||
case ACTION_GET_INFO:
|
case ACTION_GET_INFO:
|
||||||
return GetInfo(@params);
|
return GetInfo(@params);
|
||||||
case ACTION_GET_HIERARCHY:
|
case ACTION_GET_HIERARCHY:
|
||||||
return GetHierarchy(@params);
|
return GetHierarchy(@params);
|
||||||
|
case ACTION_MODIFY_CONTENTS:
|
||||||
|
return ModifyContents(@params);
|
||||||
default:
|
default:
|
||||||
return new ErrorResponse($"Unknown action: '{action}'. Valid actions are: {SupportedActions}.");
|
return new ErrorResponse($"Unknown action: '{action}'. Valid actions are: {SupportedActions}.");
|
||||||
}
|
}
|
||||||
|
|
@ -65,192 +60,6 @@ namespace MCPForUnity.Editor.Tools.Prefabs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Opens a prefab in prefab mode for editing.
|
|
||||||
/// </summary>
|
|
||||||
private static object OpenStage(JObject @params)
|
|
||||||
{
|
|
||||||
string prefabPath = @params["prefabPath"]?.ToString();
|
|
||||||
if (string.IsNullOrEmpty(prefabPath))
|
|
||||||
{
|
|
||||||
return new ErrorResponse("'prefabPath' parameter is required for open_stage.");
|
|
||||||
}
|
|
||||||
|
|
||||||
string sanitizedPath = AssetPathUtility.SanitizeAssetPath(prefabPath);
|
|
||||||
if (string.IsNullOrEmpty(sanitizedPath))
|
|
||||||
{
|
|
||||||
return new ErrorResponse($"Invalid prefab path: '{prefabPath}'.");
|
|
||||||
}
|
|
||||||
GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(sanitizedPath);
|
|
||||||
if (prefabAsset == null)
|
|
||||||
{
|
|
||||||
return new ErrorResponse($"No prefab asset found at path '{sanitizedPath}'.");
|
|
||||||
}
|
|
||||||
|
|
||||||
PrefabStage stage = PrefabStageUtility.OpenPrefab(sanitizedPath);
|
|
||||||
if (stage == null)
|
|
||||||
{
|
|
||||||
return new ErrorResponse($"Failed to open prefab stage for '{sanitizedPath}'.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new SuccessResponse($"Opened prefab stage for '{sanitizedPath}'.", SerializeStage(stage));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Closes the currently open prefab stage, optionally saving first.
|
|
||||||
/// </summary>
|
|
||||||
private static object CloseStage(JObject @params)
|
|
||||||
{
|
|
||||||
PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
|
|
||||||
if (stage == null)
|
|
||||||
{
|
|
||||||
return new SuccessResponse("No prefab stage was open.");
|
|
||||||
}
|
|
||||||
|
|
||||||
string assetPath = stage.assetPath;
|
|
||||||
bool saveBeforeClose = @params["saveBeforeClose"]?.ToObject<bool>() ?? false;
|
|
||||||
|
|
||||||
if (saveBeforeClose && stage.scene.isDirty)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
SaveAndRefreshStage(stage);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
return new ErrorResponse($"Failed to save prefab before closing: {e.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StageUtility.GoToMainStage();
|
|
||||||
return new SuccessResponse($"Closed prefab stage for '{assetPath}'.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Saves changes to the currently open prefab stage.
|
|
||||||
/// Supports a 'force' parameter for automated workflows where isDirty may not be set.
|
|
||||||
/// </summary>
|
|
||||||
private static object SaveOpenStage(JObject @params)
|
|
||||||
{
|
|
||||||
PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
|
|
||||||
if (stage == null)
|
|
||||||
{
|
|
||||||
return new ErrorResponse("No prefab stage is currently open.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ValidatePrefabStageForSave(stage))
|
|
||||||
{
|
|
||||||
return new ErrorResponse("Prefab stage validation failed. Cannot save.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for force parameter (useful for automated workflows)
|
|
||||||
bool force = @params?["force"]?.ToObject<bool>() ?? false;
|
|
||||||
|
|
||||||
// Check if there are actual changes to save
|
|
||||||
bool wasDirty = stage.scene.isDirty;
|
|
||||||
if (!wasDirty && !force)
|
|
||||||
{
|
|
||||||
return new SuccessResponse($"Prefab stage for '{stage.assetPath}' has no unsaved changes.", SerializeStage(stage));
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
SaveAndRefreshStage(stage, force);
|
|
||||||
return new SuccessResponse($"Saved prefab stage for '{stage.assetPath}'.", SerializeStage(stage));
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
return new ErrorResponse($"Failed to save prefab: {e.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Prefab Save Operations
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Saves the prefab stage and refreshes the asset database.
|
|
||||||
/// Uses PrefabUtility.SaveAsPrefabAsset for reliable prefab saving without dialogs.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="stage">The prefab stage to save.</param>
|
|
||||||
/// <param name="force">If true, marks the prefab dirty before saving to ensure changes are captured.</param>
|
|
||||||
private static void SaveAndRefreshStage(PrefabStage stage, bool force = false)
|
|
||||||
{
|
|
||||||
if (stage == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(stage), "Prefab stage cannot be null.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stage.prefabContentsRoot == null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Cannot save prefab stage without a prefab root.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(stage.assetPath))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Prefab stage has invalid asset path.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// When force=true, mark the prefab root dirty to ensure changes are saved
|
|
||||||
// This is useful for automated workflows where isDirty may not be set correctly
|
|
||||||
if (force)
|
|
||||||
{
|
|
||||||
EditorUtility.SetDirty(stage.prefabContentsRoot);
|
|
||||||
EditorSceneManager.MarkSceneDirty(stage.scene);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark all children as dirty to ensure their changes are captured
|
|
||||||
foreach (Transform child in stage.prefabContentsRoot.GetComponentsInChildren<Transform>(true))
|
|
||||||
{
|
|
||||||
if (child != stage.prefabContentsRoot.transform)
|
|
||||||
{
|
|
||||||
EditorUtility.SetDirty(child.gameObject);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use PrefabUtility.SaveAsPrefabAsset which saves without dialogs
|
|
||||||
// This is more reliable for automated workflows than EditorSceneManager.SaveScene
|
|
||||||
bool success;
|
|
||||||
PrefabUtility.SaveAsPrefabAsset(stage.prefabContentsRoot, stage.assetPath, out success);
|
|
||||||
|
|
||||||
if (!success)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Failed to save prefab asset for '{stage.assetPath}'.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure changes are persisted to disk
|
|
||||||
AssetDatabase.SaveAssets();
|
|
||||||
AssetDatabase.Refresh();
|
|
||||||
|
|
||||||
McpLog.Info($"[ManagePrefabs] Successfully saved prefab '{stage.assetPath}'.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validates prefab stage before saving.
|
|
||||||
/// </summary>
|
|
||||||
private static bool ValidatePrefabStageForSave(PrefabStage stage)
|
|
||||||
{
|
|
||||||
if (stage == null)
|
|
||||||
{
|
|
||||||
McpLog.Warn("[ManagePrefabs] No prefab stage is open.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stage.prefabContentsRoot == null)
|
|
||||||
{
|
|
||||||
McpLog.Error($"[ManagePrefabs] Prefab stage '{stage.assetPath}' has no root object.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(stage.assetPath))
|
|
||||||
{
|
|
||||||
McpLog.Error("[ManagePrefabs] Prefab stage has invalid asset path.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Create Prefab from GameObject
|
#region Create Prefab from GameObject
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -618,6 +427,307 @@ namespace MCPForUnity.Editor.Tools.Prefabs
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Headless Prefab Editing
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Modifies a prefab's contents directly without opening the prefab stage.
|
||||||
|
/// This is ideal for automated/agentic workflows as it avoids UI, dirty flags, and dialogs.
|
||||||
|
/// </summary>
|
||||||
|
private static object ModifyContents(JObject @params)
|
||||||
|
{
|
||||||
|
string prefabPath = @params["prefabPath"]?.ToString() ?? @params["path"]?.ToString();
|
||||||
|
if (string.IsNullOrEmpty(prefabPath))
|
||||||
|
{
|
||||||
|
return new ErrorResponse("'prefabPath' parameter is required for modify_contents.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string sanitizedPath = AssetPathUtility.SanitizeAssetPath(prefabPath);
|
||||||
|
if (string.IsNullOrEmpty(sanitizedPath))
|
||||||
|
{
|
||||||
|
return new ErrorResponse($"Invalid prefab path '{prefabPath}'. Path traversal sequences are not allowed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load prefab contents in isolated context (no UI)
|
||||||
|
GameObject prefabContents = PrefabUtility.LoadPrefabContents(sanitizedPath);
|
||||||
|
if (prefabContents == null)
|
||||||
|
{
|
||||||
|
return new ErrorResponse($"Failed to load prefab contents from '{sanitizedPath}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Find target object within the prefab (defaults to root)
|
||||||
|
string targetName = @params["target"]?.ToString();
|
||||||
|
GameObject targetGo = FindInPrefabContents(prefabContents, targetName);
|
||||||
|
|
||||||
|
if (targetGo == null)
|
||||||
|
{
|
||||||
|
string searchedFor = string.IsNullOrEmpty(targetName) ? "root" : $"'{targetName}'";
|
||||||
|
return new ErrorResponse($"Target {searchedFor} not found in prefab '{sanitizedPath}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply modifications
|
||||||
|
var modifyResult = ApplyModificationsToPrefabObject(targetGo, @params, prefabContents);
|
||||||
|
if (modifyResult.error != null)
|
||||||
|
{
|
||||||
|
return modifyResult.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip saving when no modifications were made to avoid unnecessary asset writes
|
||||||
|
if (!modifyResult.modified)
|
||||||
|
{
|
||||||
|
return new SuccessResponse(
|
||||||
|
$"Prefab '{sanitizedPath}' is already up to date; no changes were applied.",
|
||||||
|
new
|
||||||
|
{
|
||||||
|
prefabPath = sanitizedPath,
|
||||||
|
targetName = targetGo.name,
|
||||||
|
modified = false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the prefab
|
||||||
|
bool success;
|
||||||
|
PrefabUtility.SaveAsPrefabAsset(prefabContents, sanitizedPath, out success);
|
||||||
|
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
return new ErrorResponse($"Failed to save prefab asset at '{sanitizedPath}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetDatabase.Refresh();
|
||||||
|
|
||||||
|
McpLog.Info($"[ManagePrefabs] Successfully modified and saved prefab '{sanitizedPath}' (headless).");
|
||||||
|
|
||||||
|
return new SuccessResponse(
|
||||||
|
$"Prefab '{sanitizedPath}' modified and saved successfully.",
|
||||||
|
new
|
||||||
|
{
|
||||||
|
prefabPath = sanitizedPath,
|
||||||
|
targetName = targetGo.name,
|
||||||
|
modified = modifyResult.modified,
|
||||||
|
transform = new
|
||||||
|
{
|
||||||
|
position = new { x = targetGo.transform.localPosition.x, y = targetGo.transform.localPosition.y, z = targetGo.transform.localPosition.z },
|
||||||
|
rotation = new { x = targetGo.transform.localEulerAngles.x, y = targetGo.transform.localEulerAngles.y, z = targetGo.transform.localEulerAngles.z },
|
||||||
|
scale = new { x = targetGo.transform.localScale.x, y = targetGo.transform.localScale.y, z = targetGo.transform.localScale.z }
|
||||||
|
},
|
||||||
|
componentTypes = PrefabUtilityHelper.GetComponentTypeNames(targetGo)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Always unload prefab contents to free memory
|
||||||
|
PrefabUtility.UnloadPrefabContents(prefabContents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds a GameObject within loaded prefab contents by name or path.
|
||||||
|
/// </summary>
|
||||||
|
private static GameObject FindInPrefabContents(GameObject prefabContents, string target)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(target))
|
||||||
|
{
|
||||||
|
// Return root if no target specified
|
||||||
|
return prefabContents;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find by path first (e.g., "Parent/Child/Target")
|
||||||
|
if (target.Contains("/"))
|
||||||
|
{
|
||||||
|
Transform found = prefabContents.transform.Find(target);
|
||||||
|
if (found != null)
|
||||||
|
{
|
||||||
|
return found.gameObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If path starts with root name, try without it
|
||||||
|
if (target.StartsWith(prefabContents.name + "/"))
|
||||||
|
{
|
||||||
|
string relativePath = target.Substring(prefabContents.name.Length + 1);
|
||||||
|
found = prefabContents.transform.Find(relativePath);
|
||||||
|
if (found != null)
|
||||||
|
{
|
||||||
|
return found.gameObject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if target matches root name
|
||||||
|
if (prefabContents.name == target)
|
||||||
|
{
|
||||||
|
return prefabContents;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search by name in hierarchy
|
||||||
|
foreach (Transform t in prefabContents.GetComponentsInChildren<Transform>(true))
|
||||||
|
{
|
||||||
|
if (t.gameObject.name == target)
|
||||||
|
{
|
||||||
|
return t.gameObject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies modifications to a GameObject within loaded prefab contents.
|
||||||
|
/// Returns (modified: bool, error: ErrorResponse or null).
|
||||||
|
/// </summary>
|
||||||
|
private static (bool modified, ErrorResponse error) ApplyModificationsToPrefabObject(GameObject targetGo, JObject @params, GameObject prefabRoot)
|
||||||
|
{
|
||||||
|
bool modified = false;
|
||||||
|
|
||||||
|
// Name change
|
||||||
|
string newName = @params["name"]?.ToString();
|
||||||
|
if (!string.IsNullOrEmpty(newName) && targetGo.name != newName)
|
||||||
|
{
|
||||||
|
// If renaming the root, this will affect the prefab asset name on save
|
||||||
|
targetGo.name = newName;
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active state
|
||||||
|
bool? setActive = @params["setActive"]?.ToObject<bool?>();
|
||||||
|
if (setActive.HasValue && targetGo.activeSelf != setActive.Value)
|
||||||
|
{
|
||||||
|
targetGo.SetActive(setActive.Value);
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag
|
||||||
|
string tag = @params["tag"]?.ToString();
|
||||||
|
if (tag != null && targetGo.tag != tag)
|
||||||
|
{
|
||||||
|
string tagToSet = string.IsNullOrEmpty(tag) ? "Untagged" : tag;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
targetGo.tag = tagToSet;
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return (false, new ErrorResponse($"Failed to set tag to '{tagToSet}': {ex.Message}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layer
|
||||||
|
string layerName = @params["layer"]?.ToString();
|
||||||
|
if (!string.IsNullOrEmpty(layerName))
|
||||||
|
{
|
||||||
|
int layerId = LayerMask.NameToLayer(layerName);
|
||||||
|
if (layerId == -1)
|
||||||
|
{
|
||||||
|
return (false, new ErrorResponse($"Invalid layer specified: '{layerName}'. Use a valid layer name."));
|
||||||
|
}
|
||||||
|
if (targetGo.layer != layerId)
|
||||||
|
{
|
||||||
|
targetGo.layer = layerId;
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform: position, rotation, scale
|
||||||
|
Vector3? position = VectorParsing.ParseVector3(@params["position"]);
|
||||||
|
Vector3? rotation = VectorParsing.ParseVector3(@params["rotation"]);
|
||||||
|
Vector3? scale = VectorParsing.ParseVector3(@params["scale"]);
|
||||||
|
|
||||||
|
if (position.HasValue && targetGo.transform.localPosition != position.Value)
|
||||||
|
{
|
||||||
|
targetGo.transform.localPosition = position.Value;
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
if (rotation.HasValue && targetGo.transform.localEulerAngles != rotation.Value)
|
||||||
|
{
|
||||||
|
targetGo.transform.localEulerAngles = rotation.Value;
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
if (scale.HasValue && targetGo.transform.localScale != scale.Value)
|
||||||
|
{
|
||||||
|
targetGo.transform.localScale = scale.Value;
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parent change (within prefab hierarchy)
|
||||||
|
JToken parentToken = @params["parent"];
|
||||||
|
if (parentToken != null)
|
||||||
|
{
|
||||||
|
string parentTarget = parentToken.ToString();
|
||||||
|
Transform newParent = null;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(parentTarget))
|
||||||
|
{
|
||||||
|
GameObject parentGo = FindInPrefabContents(prefabRoot, parentTarget);
|
||||||
|
if (parentGo == null)
|
||||||
|
{
|
||||||
|
return (false, new ErrorResponse($"Parent '{parentTarget}' not found in prefab."));
|
||||||
|
}
|
||||||
|
if (parentGo.transform.IsChildOf(targetGo.transform))
|
||||||
|
{
|
||||||
|
return (false, new ErrorResponse($"Cannot parent '{targetGo.name}' to '{parentGo.name}' as it would create a hierarchy loop."));
|
||||||
|
}
|
||||||
|
newParent = parentGo.transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetGo.transform.parent != newParent)
|
||||||
|
{
|
||||||
|
targetGo.transform.SetParent(newParent, true);
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Components to add
|
||||||
|
if (@params["componentsToAdd"] is JArray componentsToAdd)
|
||||||
|
{
|
||||||
|
foreach (var compToken in componentsToAdd)
|
||||||
|
{
|
||||||
|
string typeName = compToken.Type == JTokenType.String
|
||||||
|
? compToken.ToString()
|
||||||
|
: (compToken as JObject)?["typeName"]?.ToString();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(typeName))
|
||||||
|
{
|
||||||
|
if (!ComponentResolver.TryResolve(typeName, out Type componentType, out string error))
|
||||||
|
{
|
||||||
|
return (false, new ErrorResponse($"Component type '{typeName}' not found: {error}"));
|
||||||
|
}
|
||||||
|
targetGo.AddComponent(componentType);
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Components to remove
|
||||||
|
if (@params["componentsToRemove"] is JArray componentsToRemove)
|
||||||
|
{
|
||||||
|
foreach (var compToken in componentsToRemove)
|
||||||
|
{
|
||||||
|
string typeName = compToken.ToString();
|
||||||
|
if (!string.IsNullOrEmpty(typeName))
|
||||||
|
{
|
||||||
|
if (!ComponentResolver.TryResolve(typeName, out Type componentType, out string error))
|
||||||
|
{
|
||||||
|
return (false, new ErrorResponse($"Component type '{typeName}' not found: {error}"));
|
||||||
|
}
|
||||||
|
Component comp = targetGo.GetComponent(componentType);
|
||||||
|
if (comp != null)
|
||||||
|
{
|
||||||
|
UnityEngine.Object.DestroyImmediate(comp);
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (modified, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region Hierarchy Builder
|
#region Hierarchy Builder
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -689,25 +799,5 @@ namespace MCPForUnity.Editor.Tools.Prefabs
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Serializes the prefab stage information for response.
|
|
||||||
/// </summary>
|
|
||||||
private static object SerializeStage(PrefabStage stage)
|
|
||||||
{
|
|
||||||
if (stage == null)
|
|
||||||
{
|
|
||||||
return new { isOpen = false };
|
|
||||||
}
|
|
||||||
|
|
||||||
return new
|
|
||||||
{
|
|
||||||
isOpen = true,
|
|
||||||
assetPath = stage.assetPath,
|
|
||||||
prefabRootName = stage.prefabContentsRoot != null ? stage.prefabContentsRoot.name : null,
|
|
||||||
mode = stage.mode.ToString(),
|
|
||||||
isDirty = stage.scene.isDirty
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,17 +15,16 @@ from services.tools.preflight import preflight
|
||||||
REQUIRED_PARAMS = {
|
REQUIRED_PARAMS = {
|
||||||
"get_info": ["prefab_path"],
|
"get_info": ["prefab_path"],
|
||||||
"get_hierarchy": ["prefab_path"],
|
"get_hierarchy": ["prefab_path"],
|
||||||
"open_stage": ["prefab_path"],
|
|
||||||
"create_from_gameobject": ["target", "prefab_path"],
|
"create_from_gameobject": ["target", "prefab_path"],
|
||||||
"save_open_stage": [],
|
"modify_contents": ["prefab_path"],
|
||||||
"close_stage": [],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@mcp_for_unity_tool(
|
@mcp_for_unity_tool(
|
||||||
description=(
|
description=(
|
||||||
"Manages Unity Prefab assets and stages. "
|
"Manages Unity Prefab assets via headless operations (no UI, no prefab stages). "
|
||||||
"Actions: get_info, get_hierarchy, open_stage, close_stage, save_open_stage, create_from_gameobject. "
|
"Actions: get_info, get_hierarchy, create_from_gameobject, modify_contents. "
|
||||||
|
"Use modify_contents for headless prefab editing - ideal for automated workflows. "
|
||||||
"Use manage_asset action=search filterType=Prefab to list prefabs."
|
"Use manage_asset action=search filterType=Prefab to list prefabs."
|
||||||
),
|
),
|
||||||
annotations=ToolAnnotations(
|
annotations=ToolAnnotations(
|
||||||
|
|
@ -37,27 +36,39 @@ async def manage_prefabs(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
action: Annotated[
|
action: Annotated[
|
||||||
Literal[
|
Literal[
|
||||||
"open_stage",
|
|
||||||
"close_stage",
|
|
||||||
"save_open_stage",
|
|
||||||
"create_from_gameobject",
|
"create_from_gameobject",
|
||||||
"get_info",
|
"get_info",
|
||||||
"get_hierarchy",
|
"get_hierarchy",
|
||||||
|
"modify_contents",
|
||||||
],
|
],
|
||||||
"Prefab operation to perform.",
|
"Prefab operation to perform.",
|
||||||
],
|
],
|
||||||
prefab_path: Annotated[str, "Prefab asset path (e.g., Assets/Prefabs/MyPrefab.prefab)."] | None = None,
|
prefab_path: Annotated[str, "Prefab asset path (e.g., Assets/Prefabs/MyPrefab.prefab)."] | None = None,
|
||||||
save_before_close: Annotated[bool, "Save before closing if unsaved changes exist."] | None = None,
|
target: Annotated[str, "Target GameObject: scene object for create_from_gameobject, or object within prefab for modify_contents (name or path like 'Parent/Child')."] | None = None,
|
||||||
target: Annotated[str, "Scene GameObject name for create_from_gameobject."] | None = None,
|
|
||||||
allow_overwrite: Annotated[bool, "Allow replacing existing prefab."] | None = None,
|
allow_overwrite: Annotated[bool, "Allow replacing existing prefab."] | None = None,
|
||||||
search_inactive: Annotated[bool, "Include inactive GameObjects in search."] | None = None,
|
search_inactive: Annotated[bool, "Include inactive GameObjects in search."] | None = None,
|
||||||
unlink_if_instance: Annotated[bool, "Unlink from existing prefab before creating new one."] | None = None,
|
unlink_if_instance: Annotated[bool, "Unlink from existing prefab before creating new one."] | None = None,
|
||||||
force: Annotated[bool, "Force save even if no changes detected. Useful for automated workflows."] | None = None,
|
# modify_contents parameters
|
||||||
|
position: Annotated[list[float], "New local position [x, y, z] for modify_contents."] | None = None,
|
||||||
|
rotation: Annotated[list[float], "New local rotation (euler angles) [x, y, z] for modify_contents."] | None = None,
|
||||||
|
scale: Annotated[list[float], "New local scale [x, y, z] for modify_contents."] | None = None,
|
||||||
|
name: Annotated[str, "New name for the target object in modify_contents."] | None = None,
|
||||||
|
tag: Annotated[str, "New tag for the target object in modify_contents."] | None = None,
|
||||||
|
layer: Annotated[str, "New layer name for the target object in modify_contents."] | None = None,
|
||||||
|
set_active: Annotated[bool, "Set active state of target object in modify_contents."] | None = None,
|
||||||
|
parent: Annotated[str, "New parent object name/path within prefab for modify_contents."] | None = None,
|
||||||
|
components_to_add: Annotated[list[str], "Component types to add in modify_contents."] | None = None,
|
||||||
|
components_to_remove: Annotated[list[str], "Component types to remove in modify_contents."] | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
|
# Back-compat: map 'name' → 'target' for create_from_gameobject (Unity accepts both)
|
||||||
|
if action == "create_from_gameobject" and target is None and name is not None:
|
||||||
|
target = name
|
||||||
|
|
||||||
# Validate required parameters
|
# Validate required parameters
|
||||||
required = REQUIRED_PARAMS.get(action, [])
|
required = REQUIRED_PARAMS.get(action, [])
|
||||||
for param_name in required:
|
for param_name in required:
|
||||||
param_value = locals().get(param_name)
|
# Use updated local value for target after back-compat mapping
|
||||||
|
param_value = target if param_name == "target" else locals().get(param_name)
|
||||||
# Check for None and empty/whitespace strings
|
# Check for None and empty/whitespace strings
|
||||||
if param_value is None or (isinstance(param_value, str) and not param_value.strip()):
|
if param_value is None or (isinstance(param_value, str) and not param_value.strip()):
|
||||||
return {
|
return {
|
||||||
|
|
@ -86,11 +97,6 @@ async def manage_prefabs(
|
||||||
if prefab_path:
|
if prefab_path:
|
||||||
params["prefabPath"] = prefab_path
|
params["prefabPath"] = prefab_path
|
||||||
|
|
||||||
# Handle boolean parameters with proper coercion
|
|
||||||
save_before_close_val = coerce_bool(save_before_close)
|
|
||||||
if save_before_close_val is not None:
|
|
||||||
params["saveBeforeClose"] = save_before_close_val
|
|
||||||
|
|
||||||
if target:
|
if target:
|
||||||
params["target"] = target
|
params["target"] = target
|
||||||
|
|
||||||
|
|
@ -106,9 +112,28 @@ async def manage_prefabs(
|
||||||
if unlink_if_instance_val is not None:
|
if unlink_if_instance_val is not None:
|
||||||
params["unlinkIfInstance"] = unlink_if_instance_val
|
params["unlinkIfInstance"] = unlink_if_instance_val
|
||||||
|
|
||||||
force_val = coerce_bool(force)
|
# modify_contents parameters
|
||||||
if force_val is not None:
|
if position is not None:
|
||||||
params["force"] = force_val
|
params["position"] = position
|
||||||
|
if rotation is not None:
|
||||||
|
params["rotation"] = rotation
|
||||||
|
if scale is not None:
|
||||||
|
params["scale"] = scale
|
||||||
|
if name is not None:
|
||||||
|
params["name"] = name
|
||||||
|
if tag is not None:
|
||||||
|
params["tag"] = tag
|
||||||
|
if layer is not None:
|
||||||
|
params["layer"] = layer
|
||||||
|
set_active_val = coerce_bool(set_active)
|
||||||
|
if set_active_val is not None:
|
||||||
|
params["setActive"] = set_active_val
|
||||||
|
if parent is not None:
|
||||||
|
params["parent"] = parent
|
||||||
|
if components_to_add is not None:
|
||||||
|
params["componentsToAdd"] = components_to_add
|
||||||
|
if components_to_remove is not None:
|
||||||
|
params["componentsToRemove"] = components_to_remove
|
||||||
|
|
||||||
# Send command to Unity
|
# Send command to Unity
|
||||||
response = await send_with_unity_instance(
|
response = await send_with_unity_instance(
|
||||||
|
|
@ -137,4 +162,4 @@ async def manage_prefabs(
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"message": f"Error managing prefabs: {exc}"
|
"message": f"Error managing prefabs: {exc}"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue