Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion MCPForUnity/Editor/Tools/GameObjects/ManageGameObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public static object HandleCommand(JObject @params)
return new ErrorResponse(
$"Target '{targetPath}' is a prefab asset. " +
$"Use 'manage_asset' with action='modify' for prefab asset modifications, " +
$"or 'manage_prefabs' with action='modify_contents' to edit the prefab headlessly, or 'manage_editor' with action='close_prefab_stage' to exit prefab editing mode."
$"or 'manage_prefabs' with action='modify_contents' to edit the prefab headlessly, or 'manage_prefabs' with action='close_prefab_stage' to exit prefab editing mode."
);
}
// --- End Prefab Asset Check ---
Expand Down
119 changes: 1 addition & 118 deletions MCPForUnity/Editor/Tools/ManageEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using MCPForUnity.Editor.Services;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEditorInternal; // Required for tag management
using UnityEngine;

Expand Down Expand Up @@ -47,8 +46,6 @@ public static object HandleCommand(JObject @params)
// Parameters for specific actions
string tagName = p.Get("tagName");
string layerName = p.Get("layerName");
string prefabPath = p.Get("prefabPath") ?? p.Get("path");

// Route action
switch (action)
{
Expand Down Expand Up @@ -137,14 +134,6 @@ public static object HandleCommand(JObject @params)
// // Handle string name or int index
// return SetQualityLevel(@params["qualityLevel"]);

// Prefab Stage
case "open_prefab_stage":
return OpenPrefabStage(prefabPath);
case "save_prefab_stage":
return SavePrefabStage();
case "close_prefab_stage":
return ClosePrefabStage();

// Package Deployment
case "deploy_package":
return DeployPackage();
Expand Down Expand Up @@ -182,7 +171,7 @@ public static object HandleCommand(JObject @params)

default:
return new ErrorResponse(
$"Unknown action: '{action}'. Supported actions: play, pause, stop, set_active_tool, add_tag, remove_tag, add_layer, remove_layer, open_prefab_stage, save_prefab_stage, close_prefab_stage, deploy_package, restore_package, undo, redo. Use MCP resources for reading editor state, project info, tags, layers, selection, windows, prefab stage, and active tool."
$"Unknown action: '{action}'. Supported actions: play, pause, stop, set_active_tool, add_tag, remove_tag, add_layer, remove_layer, deploy_package, restore_package, undo, redo. For prefab editing (open/save/close prefab stage), use manage_prefabs. Use MCP resources for reading editor state, project info, tags, layers, selection, windows, prefab stage, and active tool."
);
}
}
Expand Down Expand Up @@ -402,112 +391,6 @@ private static object RemoveLayer(string layerName)
}
}

// --- Prefab Stage Methods ---

private static object OpenPrefabStage(string requestedPath)
{
if (string.IsNullOrWhiteSpace(requestedPath))
{
return new ErrorResponse("'prefabPath' parameter is required for open_prefab_stage.");
}

string sanitizedPath = AssetPathUtility.SanitizeAssetPath(requestedPath);
if (sanitizedPath == null)
{
return new ErrorResponse($"Invalid prefab path (path traversal detected): '{requestedPath}'.");
}

if (!sanitizedPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
{
return new ErrorResponse($"Prefab path must be within the Assets folder. Got: '{sanitizedPath}'.");
}

if (!sanitizedPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
{
return new ErrorResponse($"Prefab path must end with '.prefab'. Got: '{sanitizedPath}'.");
}

try
{
GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(sanitizedPath);
if (prefabAsset == null)
{
return new ErrorResponse($"Prefab asset not found at '{sanitizedPath}'.");
}

var prefabStage = PrefabStageUtility.OpenPrefab(sanitizedPath);
bool enteredStage = prefabStage != null
&& string.Equals(prefabStage.assetPath, sanitizedPath, StringComparison.OrdinalIgnoreCase)
&& prefabStage.prefabContentsRoot != null;

if (!enteredStage)
{
return new ErrorResponse($"Failed to open prefab stage for '{sanitizedPath}'. PrefabStageUtility.OpenPrefab did not enter the requested prefab stage.");
}

return new SuccessResponse(
$"Opened prefab stage for '{sanitizedPath}'.",
new
{
prefabPath = sanitizedPath,
openedPrefabPath = prefabStage.assetPath,
rootName = prefabStage.prefabContentsRoot.name,
enteredPrefabStage = enteredStage
}
);
}
catch (Exception e)
{
return new ErrorResponse($"Error opening prefab stage: {e.Message}");
}
}

private static object SavePrefabStage()
{
try
{
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
if (prefabStage == null)
{
return new ErrorResponse("Not currently in prefab editing mode. Open a prefab stage first with open_prefab_stage.");
}

string prefabPath = prefabStage.assetPath;
EditorSceneManager.MarkSceneDirty(prefabStage.scene);
bool saved = EditorSceneManager.SaveScene(prefabStage.scene);
if (!saved)
{
return new ErrorResponse($"Failed to save prefab stage for '{prefabPath}'. The file may be read-only or the disk may be full.");
}

return new SuccessResponse($"Saved prefab stage changes for '{prefabPath}'.", new { prefabPath, saved });
}
catch (Exception e)
{
return new ErrorResponse($"Error saving prefab stage: {e.Message}");
}
}

private static object ClosePrefabStage()
{
try
{
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
if (prefabStage == null)
{
return new SuccessResponse("Not currently in prefab editing mode.");
}

string prefabPath = prefabStage.assetPath;
StageUtility.GoToMainStage();
return new SuccessResponse($"Exited prefab stage for '{prefabPath}'.", new { prefabPath });
}
catch (Exception e)
{
return new ErrorResponse($"Error closing prefab stage: {e.Message}");
}
}

// --- Package Deployment Methods ---

private static object DeployPackage()
Expand Down
138 changes: 135 additions & 3 deletions MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ namespace MCPForUnity.Editor.Tools.Prefabs
{
[McpForUnityTool("manage_prefabs", AutoRegister = false)]
/// <summary>
/// Tool to manage Unity Prefabs: create, inspect, and modify prefab assets.
/// Uses headless editing (no UI, no dialogs) for reliable automated workflows.
/// Tool to manage Unity Prefabs: create, inspect, modify, and open/save/close prefab stage.
/// Supports both headless editing (modify_contents) and interactive prefab stage workflows.
/// </summary>
public static class ManagePrefabs
{
Expand All @@ -23,7 +23,10 @@ public static class ManagePrefabs
private const string ACTION_GET_INFO = "get_info";
private const string ACTION_GET_HIERARCHY = "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;
private const string ACTION_OPEN_PREFAB_STAGE = "open_prefab_stage";
private const string ACTION_SAVE_PREFAB_STAGE = "save_prefab_stage";
private const string ACTION_CLOSE_PREFAB_STAGE = "close_prefab_stage";
private const string SupportedActions = ACTION_CREATE_FROM_GAMEOBJECT + ", " + ACTION_GET_INFO + ", " + ACTION_GET_HIERARCHY + ", " + ACTION_MODIFY_CONTENTS + ", " + ACTION_OPEN_PREFAB_STAGE + ", " + ACTION_SAVE_PREFAB_STAGE + ", " + ACTION_CLOSE_PREFAB_STAGE;

public static object HandleCommand(JObject @params)
{
Expand All @@ -50,6 +53,18 @@ public static object HandleCommand(JObject @params)
return GetHierarchy(@params);
case ACTION_MODIFY_CONTENTS:
return ModifyContents(@params);
case ACTION_OPEN_PREFAB_STAGE:
{
string prefabPath = @params["prefabPath"]?.ToString() ?? @params["path"]?.ToString();
return OpenPrefabStage(prefabPath);
}
case ACTION_SAVE_PREFAB_STAGE:
return SavePrefabStage();
case ACTION_CLOSE_PREFAB_STAGE:
{
bool saveBeforeClose = @params["saveBeforeClose"]?.ToObject<bool>() ?? false;
return ClosePrefabStage(saveBeforeClose);
}
default:
return new ErrorResponse($"Unknown action: '{action}'. Valid actions are: {SupportedActions}.");
}
Expand Down Expand Up @@ -1261,5 +1276,122 @@ private static void BuildHierarchyItemsRecursive(Transform transform, Transform
}

#endregion

#region Prefab Stage

private static object OpenPrefabStage(string requestedPath)
{
if (string.IsNullOrWhiteSpace(requestedPath))
{
return new ErrorResponse("Either 'prefabPath' or 'path' parameter is required for open_prefab_stage.");
}

string sanitizedPath = AssetPathUtility.SanitizeAssetPath(requestedPath);
if (sanitizedPath == null)
{
return new ErrorResponse($"Invalid prefab path (path traversal detected): '{requestedPath}'.");
}

if (!sanitizedPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
{
return new ErrorResponse($"Prefab path must be within the Assets folder. Got: '{sanitizedPath}'.");
}

if (!sanitizedPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
{
return new ErrorResponse($"Prefab path must end with '.prefab'. Got: '{sanitizedPath}'.");
}

try
{
GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(sanitizedPath);
if (prefabAsset == null)
{
return new ErrorResponse($"Prefab asset not found at '{sanitizedPath}'.");
}

var prefabStage = PrefabStageUtility.OpenPrefab(sanitizedPath);
bool enteredStage = prefabStage != null
&& string.Equals(prefabStage.assetPath, sanitizedPath, StringComparison.OrdinalIgnoreCase)
&& prefabStage.prefabContentsRoot != null;

if (!enteredStage)
{
return new ErrorResponse($"Failed to open prefab stage for '{sanitizedPath}'. PrefabStageUtility.OpenPrefab did not enter the requested prefab stage.");
}

return new SuccessResponse(
$"Opened prefab stage for '{sanitizedPath}'.",
new
{
prefabPath = sanitizedPath,
openedPrefabPath = prefabStage.assetPath,
rootName = prefabStage.prefabContentsRoot.name,
enteredPrefabStage = enteredStage
}
);
}
catch (Exception e)
{
return new ErrorResponse($"Error opening prefab stage: {e.Message}");
}
}

private static object SavePrefabStage()
{
try
{
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
if (prefabStage == null)
{
return new ErrorResponse("Not currently in prefab editing mode. Open a prefab stage first with open_prefab_stage.");
}

string prefabPath = prefabStage.assetPath;
EditorSceneManager.MarkSceneDirty(prefabStage.scene);
bool saved = EditorSceneManager.SaveScene(prefabStage.scene);
if (!saved)
{
return new ErrorResponse($"Failed to save prefab stage for '{prefabPath}'. The file may be read-only or the disk may be full.");
}

return new SuccessResponse($"Saved prefab stage changes for '{prefabPath}'.", new { prefabPath, saved });
}
catch (Exception e)
{
return new ErrorResponse($"Error saving prefab stage: {e.Message}");
}
}

private static object ClosePrefabStage(bool saveBeforeClose = false)
{
try
{
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
if (prefabStage == null)
{
return new SuccessResponse("Not currently in prefab editing mode.");
}

if (saveBeforeClose)
{
var saveResult = SavePrefabStage();
if (saveResult is ErrorResponse)
{
return saveResult;
}
}

string prefabPath = prefabStage.assetPath;
StageUtility.GoToMainStage();
return new SuccessResponse($"Exited prefab stage for '{prefabPath}'.", new { prefabPath });
}
catch (Exception e)
{
return new ErrorResponse($"Error closing prefab stage: {e.Message}");
}
}

#endregion
}
}
6 changes: 3 additions & 3 deletions Server/src/cli/commands/prefab.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def open_stage(path: str):
config = get_config()

params: dict[str, Any] = {
"action": "open_stage",
"action": "open_prefab_stage",
"prefabPath": path,
}

Expand Down Expand Up @@ -57,7 +57,7 @@ def close_stage(save: bool):
config = get_config()

params: dict[str, Any] = {
"action": "close_stage",
"action": "close_prefab_stage",
}
if save:
params["saveBeforeClose"] = True
Expand All @@ -83,7 +83,7 @@ def save_stage():
"action": "save_prefab_stage",
}

result = run_command("manage_editor", params, config)
result = run_command("manage_prefabs", params, config)
click.echo(format_output(result, config.format))
if result.get("success"):
print_success("Saved prefab stage")
Expand Down
6 changes: 3 additions & 3 deletions Server/src/services/resources/prefab.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ async def get_prefab_api_docs(_ctx: Context) -> MCPResponse:
"workflow": [
"1. Use manage_asset action=search filterType=Prefab to find prefabs",
"2. Use the asset path to access detailed data via resources below",
"3. Use manage_editor action=open_prefab_stage / save_prefab_stage / close_prefab_stage for prefab editing UI transitions"
"3. Use manage_prefabs action=open_prefab_stage / save_prefab_stage / close_prefab_stage for prefab editing UI transitions"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix contradictory prefab tool guidance in this resource payload.

Line 60 correctly routes stage lifecycle to manage_prefabs, but related_tools still says manage_editor owns open/save/close stages. This inconsistency can send callers to unsupported actions.

Suggested doc fix
         "related_tools": {
-            "manage_editor": "Open/save/close prefab stages in the Unity Editor UI",
-            "manage_prefabs": "Headless prefab inspection and modification without opening prefab stages",
+            "manage_editor": "Editor controls (play/pause/stop, active tool, tags/layers, package deploy/restore)",
+            "manage_prefabs": "Prefab stage lifecycle (open/save/close) and headless prefab inspection/modification",
             "manage_asset": "Search for prefab assets, get asset info",
             "manage_gameobject": "Modify GameObjects in open prefab stage",
             "manage_components": "Add/remove/modify components on prefab GameObjects"
         }

Also applies to: 83-85

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Server/src/services/resources/prefab.py` at line 60, The resource payload
text references stage lifecycle actions under manage_prefabs but the
related_tools field still lists manage_editor, causing inconsistency; update the
related_tools entries that refer to "manage_editor" (the ones associated with
the prefab stage guidance string "Use manage_prefabs action=open_prefab_stage /
save_prefab_stage / close_prefab_stage") to "manage_prefabs" in the prefab
resource definition (also fix the same mismatch at the other occurrence around
lines 83–85) so callers are guided to the correct action owner.

],
"path_encoding": {
"note": "Prefab paths must be URL-encoded when used in resource URIs",
Expand All @@ -80,8 +80,8 @@ async def get_prefab_api_docs(_ctx: Context) -> MCPResponse:
}
},
"related_tools": {
"manage_editor": "Open/save/close prefab stages in the Unity Editor UI",
"manage_prefabs": "Headless prefab inspection and modification without opening prefab stages",
"manage_editor": "Editor controls (play/pause/stop, active tool, tags/layers, package deploy/restore)",
"manage_prefabs": "Prefab stage lifecycle (open/save/close) and headless prefab inspection/modification",
"manage_asset": "Search for prefab assets, get asset info",
"manage_gameobject": "Modify GameObjects in open prefab stage",
"manage_components": "Add/remove/modify components on prefab GameObjects"
Expand Down
Loading