unity-mcp/UnityMcpBridge/Editor/Tools/Prefabs/ManagePrefabs.cs

276 lines
10 KiB
C#
Raw Normal View History

using System;
using System.IO;
using MCPForUnity.Editor.Helpers;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace MCPForUnity.Editor.Tools.Prefabs
{
Make it easier to add tools (#301) * Add a decorate that wraps around the `mcp.tool` decorator. This will allow us to more easily collect tools * Register tools that's defined in the tools folder * Update Python tools to use new decorator * Convert script_apply_edits tool * Convert last remaining tools with new decorator * Create an attribute so we can identify tools via Reflection * Add attribute to all C# tools * Use reflection to load tools * Initialize command registry to load tools at startup * Update tests * Move Dev docs to docs folder * Add docs for adding custom tools * Update function docs for Python decorator * Add working example of adding a screenshot tool * docs: update relative links in README files Updated the relative links in both README-DEV.md and README-DEV-zh.md to use direct filenames instead of paths relative to the docs directory, improving link correctness when files are accessed from the root directory. * docs: update telemetry documentation path reference Updated the link to TELEMETRY.md in README.md to point to the new docs/ directory location to ensure users can access the telemetry documentation correctly. Also moved the TELEMETRY.md file to the docs/ directory as part of the documentation restructuring. * rename CursorHelp.md to docs/CURSOR_HELP.md Moved the CursorHelp.md file to the docs directory to better organize documentation files and improve project structure. * docs: update CUSTOM_TOOLS.md with improved tool naming documentation and path corrections - Clarified that the `name` argument in `@mcp_for_unity_tool` decorator is optional and defaults to the function name - Added documentation about using all FastMCP `mcp.tool` function decorator options - Updated class naming documentation to mention snake_case conversion by default - Corrected Python file path from `tools/screenshot_tool.py` to `UnityMcpServer~/src/tools/screenshot_tool.py` - Enhanced documentation for tool discovery and usage examples * docs: restructure development documentation and add custom tools guide Rearranged the development section in README.md to better organize the documentation flow. Added a dedicated section for "Adding Custom Tools" with a link to the new CUSTOM_TOOLS.md file, and renamed the previous "For Developers" section to "Contributing to the Project" to better reflect its content. This improves discoverability and organization of the development setup documentation. * docs: update developer documentation and add README links - Added links to developer READMEs in CUSTOM_TOOLS.md to guide users to the appropriate documentation - Fixed typo in README-DEV.md ("roote" → "root") for improved clarity - These changes improve the developer experience by providing better documentation navigation and correcting technical inaccuracies * feat(tools): enhance tool registration with wrapped function assignment Updated the tool registration process to properly chain the mcp.tool decorator and telemetry wrapper, ensuring the wrapped function is correctly assigned to tool_info['func'] for proper tool execution and telemetry tracking. This change improves the reliability of tool registration and monitoring. * Remove AI generated code that was never used... * feat: Rebuild MCP server installation with embedded source Refactored the server repair logic to implement a full rebuild of the MCP server installation using the embedded source. The new RebuildMcpServer method now: - Uses embedded server source instead of attempting repair of existing installation - Deletes the entire existing server directory before re-copying - Handles UV process cleanup for the target path - Simplifies the installation flow by removing the complex Python environment repair logic - Maintains the same installation behavior but with a cleaner, more reliable rebuild approach This change improves reliability of server installations by ensuring a clean slate rebuild rather than attempting to repair potentially corrupted environments. * Add the rebuild server step * docs: clarify tool description field requirements and client compatibility * fix: move initialization flag after tool discovery to prevent race conditions * refactor: remove redundant TryParseVersion overrides in platform detectors * refactor: remove duplicate UV validation code from platform detectors * Update UnityMcpBridge/Editor/Tools/CommandRegistry.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * refactor: replace WriteToConfig reflection with direct McpConfigurationHelper call --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-10-04 06:53:09 +08:00
[McpForUnityTool("manage_prefabs")]
public static class ManagePrefabs
{
private const string SupportedActions = "open_stage, close_stage, save_open_stage, create_from_gameobject";
public static object HandleCommand(JObject @params)
{
if (@params == null)
{
return Response.Error("Parameters cannot be null.");
}
string action = @params["action"]?.ToString()?.ToLowerInvariant();
if (string.IsNullOrEmpty(action))
{
return Response.Error($"Action parameter is required. Valid actions are: {SupportedActions}.");
}
try
{
switch (action)
{
case "open_stage":
return OpenStage(@params);
case "close_stage":
return CloseStage(@params);
case "save_open_stage":
return SaveOpenStage();
case "create_from_gameobject":
return CreatePrefabFromGameObject(@params);
default:
return Response.Error($"Unknown action: '{action}'. Valid actions are: {SupportedActions}.");
}
}
catch (Exception e)
{
McpLog.Error($"[ManagePrefabs] Action '{action}' failed: {e}");
return Response.Error($"Internal error: {e.Message}");
}
}
private static object OpenStage(JObject @params)
{
string prefabPath = @params["prefabPath"]?.ToString();
if (string.IsNullOrEmpty(prefabPath))
{
return Response.Error("'prefabPath' parameter is required for open_stage.");
}
string sanitizedPath = AssetPathUtility.SanitizeAssetPath(prefabPath);
GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(sanitizedPath);
if (prefabAsset == null)
{
return Response.Error($"No prefab asset found at path '{sanitizedPath}'.");
}
string modeValue = @params["mode"]?.ToString();
if (!string.IsNullOrEmpty(modeValue) && !modeValue.Equals(PrefabStage.Mode.InIsolation.ToString(), StringComparison.OrdinalIgnoreCase))
{
return Response.Error("Only PrefabStage mode 'InIsolation' is supported at this time.");
}
PrefabStage stage = PrefabStageUtility.OpenPrefab(sanitizedPath);
if (stage == null)
{
return Response.Error($"Failed to open prefab stage for '{sanitizedPath}'.");
}
return Response.Success($"Opened prefab stage for '{sanitizedPath}'.", SerializeStage(stage));
}
private static object CloseStage(JObject @params)
{
PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
if (stage == null)
{
return Response.Success("No prefab stage was open.");
}
bool saveBeforeClose = @params["saveBeforeClose"]?.ToObject<bool>() ?? false;
if (saveBeforeClose && stage.scene.isDirty)
{
SaveStagePrefab(stage);
AssetDatabase.SaveAssets();
}
StageUtility.GoToMainStage();
return Response.Success($"Closed prefab stage for '{stage.assetPath}'.");
}
private static object SaveOpenStage()
{
PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
if (stage == null)
{
return Response.Error("No prefab stage is currently open.");
}
SaveStagePrefab(stage);
AssetDatabase.SaveAssets();
return Response.Success($"Saved prefab stage for '{stage.assetPath}'.", SerializeStage(stage));
}
private static void SaveStagePrefab(PrefabStage stage)
{
if (stage?.prefabContentsRoot == null)
{
throw new InvalidOperationException("Cannot save prefab stage without a prefab root.");
}
bool saved = PrefabUtility.SaveAsPrefabAsset(stage.prefabContentsRoot, stage.assetPath);
if (!saved)
{
throw new InvalidOperationException($"Failed to save prefab asset at '{stage.assetPath}'.");
}
}
private static object CreatePrefabFromGameObject(JObject @params)
{
string targetName = @params["target"]?.ToString() ?? @params["name"]?.ToString();
if (string.IsNullOrEmpty(targetName))
{
return Response.Error("'target' parameter is required for create_from_gameobject.");
}
bool includeInactive = @params["searchInactive"]?.ToObject<bool>() ?? false;
GameObject sourceObject = FindSceneObjectByName(targetName, includeInactive);
if (sourceObject == null)
{
return Response.Error($"GameObject '{targetName}' not found in the active scene.");
}
if (PrefabUtility.IsPartOfPrefabAsset(sourceObject))
{
return Response.Error(
$"GameObject '{sourceObject.name}' is part of a prefab asset. Open the prefab stage to save changes instead."
);
}
PrefabInstanceStatus status = PrefabUtility.GetPrefabInstanceStatus(sourceObject);
if (status != PrefabInstanceStatus.NotAPrefab)
{
return Response.Error(
$"GameObject '{sourceObject.name}' is already linked to an existing prefab instance."
);
}
string requestedPath = @params["prefabPath"]?.ToString();
if (string.IsNullOrWhiteSpace(requestedPath))
{
return Response.Error("'prefabPath' parameter is required for create_from_gameobject.");
}
string sanitizedPath = AssetPathUtility.SanitizeAssetPath(requestedPath);
if (!sanitizedPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
{
sanitizedPath += ".prefab";
}
bool allowOverwrite = @params["allowOverwrite"]?.ToObject<bool>() ?? false;
string finalPath = sanitizedPath;
if (!allowOverwrite && AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(finalPath) != null)
{
finalPath = AssetDatabase.GenerateUniqueAssetPath(finalPath);
}
EnsureAssetDirectoryExists(finalPath);
try
{
GameObject connectedInstance = PrefabUtility.SaveAsPrefabAssetAndConnect(
sourceObject,
finalPath,
InteractionMode.AutomatedAction
);
if (connectedInstance == null)
{
return Response.Error($"Failed to save prefab asset at '{finalPath}'.");
}
Selection.activeGameObject = connectedInstance;
return Response.Success(
$"Prefab created at '{finalPath}' and instance linked.",
new
{
prefabPath = finalPath,
instanceId = connectedInstance.GetInstanceID()
}
);
}
catch (Exception e)
{
return Response.Error($"Error saving prefab asset at '{finalPath}': {e.Message}");
}
}
private static void EnsureAssetDirectoryExists(string assetPath)
{
string directory = Path.GetDirectoryName(assetPath);
if (string.IsNullOrEmpty(directory))
{
return;
}
string fullDirectory = Path.Combine(Directory.GetCurrentDirectory(), directory);
if (!Directory.Exists(fullDirectory))
{
Directory.CreateDirectory(fullDirectory);
AssetDatabase.Refresh();
}
}
private static GameObject FindSceneObjectByName(string name, bool includeInactive)
{
PrefabStage stage = PrefabStageUtility.GetCurrentPrefabStage();
if (stage?.prefabContentsRoot != null)
{
foreach (Transform transform in stage.prefabContentsRoot.GetComponentsInChildren<Transform>(includeInactive))
{
if (transform.name == name)
{
return transform.gameObject;
}
}
}
Scene activeScene = SceneManager.GetActiveScene();
foreach (GameObject root in activeScene.GetRootGameObjects())
{
foreach (Transform transform in root.GetComponentsInChildren<Transform>(includeInactive))
{
GameObject candidate = transform.gameObject;
if (candidate.name == name)
{
return candidate;
}
}
}
return null;
}
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
};
}
}
}