Skip to content

Commit e4f5762

Browse files
authored
Merge pull request #1013 from Scriptwonder/prefab-fix
Prefab stages integration
2 parents 8e3f721 + 8b913ed commit e4f5762

13 files changed

Lines changed: 242 additions & 268 deletions

File tree

MCPForUnity/Editor/Tools/GameObjects/ManageGameObject.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public static object HandleCommand(JObject @params)
8080
return new ErrorResponse(
8181
$"Target '{targetPath}' is a prefab asset. " +
8282
$"Use 'manage_asset' with action='modify' for prefab asset modifications, " +
83-
$"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."
83+
$"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."
8484
);
8585
}
8686
// --- End Prefab Asset Check ---

MCPForUnity/Editor/Tools/ManageEditor.cs

Lines changed: 1 addition & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
using MCPForUnity.Editor.Services;
44
using Newtonsoft.Json.Linq;
55
using UnityEditor;
6-
using UnityEditor.SceneManagement;
76
using UnityEditorInternal; // Required for tag management
87
using UnityEngine;
98

@@ -47,8 +46,6 @@ public static object HandleCommand(JObject @params)
4746
// Parameters for specific actions
4847
string tagName = p.Get("tagName");
4948
string layerName = p.Get("layerName");
50-
string prefabPath = p.Get("prefabPath") ?? p.Get("path");
51-
5249
// Route action
5350
switch (action)
5451
{
@@ -137,14 +134,6 @@ public static object HandleCommand(JObject @params)
137134
// // Handle string name or int index
138135
// return SetQualityLevel(@params["qualityLevel"]);
139136

140-
// Prefab Stage
141-
case "open_prefab_stage":
142-
return OpenPrefabStage(prefabPath);
143-
case "save_prefab_stage":
144-
return SavePrefabStage();
145-
case "close_prefab_stage":
146-
return ClosePrefabStage();
147-
148137
// Package Deployment
149138
case "deploy_package":
150139
return DeployPackage();
@@ -182,7 +171,7 @@ public static object HandleCommand(JObject @params)
182171

183172
default:
184173
return new ErrorResponse(
185-
$"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."
174+
$"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."
186175
);
187176
}
188177
}
@@ -402,112 +391,6 @@ private static object RemoveLayer(string layerName)
402391
}
403392
}
404393

405-
// --- Prefab Stage Methods ---
406-
407-
private static object OpenPrefabStage(string requestedPath)
408-
{
409-
if (string.IsNullOrWhiteSpace(requestedPath))
410-
{
411-
return new ErrorResponse("'prefabPath' parameter is required for open_prefab_stage.");
412-
}
413-
414-
string sanitizedPath = AssetPathUtility.SanitizeAssetPath(requestedPath);
415-
if (sanitizedPath == null)
416-
{
417-
return new ErrorResponse($"Invalid prefab path (path traversal detected): '{requestedPath}'.");
418-
}
419-
420-
if (!sanitizedPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
421-
{
422-
return new ErrorResponse($"Prefab path must be within the Assets folder. Got: '{sanitizedPath}'.");
423-
}
424-
425-
if (!sanitizedPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
426-
{
427-
return new ErrorResponse($"Prefab path must end with '.prefab'. Got: '{sanitizedPath}'.");
428-
}
429-
430-
try
431-
{
432-
GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(sanitizedPath);
433-
if (prefabAsset == null)
434-
{
435-
return new ErrorResponse($"Prefab asset not found at '{sanitizedPath}'.");
436-
}
437-
438-
var prefabStage = PrefabStageUtility.OpenPrefab(sanitizedPath);
439-
bool enteredStage = prefabStage != null
440-
&& string.Equals(prefabStage.assetPath, sanitizedPath, StringComparison.OrdinalIgnoreCase)
441-
&& prefabStage.prefabContentsRoot != null;
442-
443-
if (!enteredStage)
444-
{
445-
return new ErrorResponse($"Failed to open prefab stage for '{sanitizedPath}'. PrefabStageUtility.OpenPrefab did not enter the requested prefab stage.");
446-
}
447-
448-
return new SuccessResponse(
449-
$"Opened prefab stage for '{sanitizedPath}'.",
450-
new
451-
{
452-
prefabPath = sanitizedPath,
453-
openedPrefabPath = prefabStage.assetPath,
454-
rootName = prefabStage.prefabContentsRoot.name,
455-
enteredPrefabStage = enteredStage
456-
}
457-
);
458-
}
459-
catch (Exception e)
460-
{
461-
return new ErrorResponse($"Error opening prefab stage: {e.Message}");
462-
}
463-
}
464-
465-
private static object SavePrefabStage()
466-
{
467-
try
468-
{
469-
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
470-
if (prefabStage == null)
471-
{
472-
return new ErrorResponse("Not currently in prefab editing mode. Open a prefab stage first with open_prefab_stage.");
473-
}
474-
475-
string prefabPath = prefabStage.assetPath;
476-
EditorSceneManager.MarkSceneDirty(prefabStage.scene);
477-
bool saved = EditorSceneManager.SaveScene(prefabStage.scene);
478-
if (!saved)
479-
{
480-
return new ErrorResponse($"Failed to save prefab stage for '{prefabPath}'. The file may be read-only or the disk may be full.");
481-
}
482-
483-
return new SuccessResponse($"Saved prefab stage changes for '{prefabPath}'.", new { prefabPath, saved });
484-
}
485-
catch (Exception e)
486-
{
487-
return new ErrorResponse($"Error saving prefab stage: {e.Message}");
488-
}
489-
}
490-
491-
private static object ClosePrefabStage()
492-
{
493-
try
494-
{
495-
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
496-
if (prefabStage == null)
497-
{
498-
return new SuccessResponse("Not currently in prefab editing mode.");
499-
}
500-
501-
string prefabPath = prefabStage.assetPath;
502-
StageUtility.GoToMainStage();
503-
return new SuccessResponse($"Exited prefab stage for '{prefabPath}'.", new { prefabPath });
504-
}
505-
catch (Exception e)
506-
{
507-
return new ErrorResponse($"Error closing prefab stage: {e.Message}");
508-
}
509-
}
510-
511394
// --- Package Deployment Methods ---
512395

513396
private static object DeployPackage()

MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs

Lines changed: 135 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ namespace MCPForUnity.Editor.Tools.Prefabs
1313
{
1414
[McpForUnityTool("manage_prefabs", AutoRegister = false)]
1515
/// <summary>
16-
/// Tool to manage Unity Prefabs: create, inspect, and modify prefab assets.
17-
/// Uses headless editing (no UI, no dialogs) for reliable automated workflows.
16+
/// Tool to manage Unity Prefabs: create, inspect, modify, and open/save/close prefab stage.
17+
/// Supports both headless editing (modify_contents) and interactive prefab stage workflows.
1818
/// </summary>
1919
public static class ManagePrefabs
2020
{
@@ -23,7 +23,10 @@ public static class ManagePrefabs
2323
private const string ACTION_GET_INFO = "get_info";
2424
private const string ACTION_GET_HIERARCHY = "get_hierarchy";
2525
private const string ACTION_MODIFY_CONTENTS = "modify_contents";
26-
private const string SupportedActions = ACTION_CREATE_FROM_GAMEOBJECT + ", " + ACTION_GET_INFO + ", " + ACTION_GET_HIERARCHY + ", " + ACTION_MODIFY_CONTENTS;
26+
private const string ACTION_OPEN_PREFAB_STAGE = "open_prefab_stage";
27+
private const string ACTION_SAVE_PREFAB_STAGE = "save_prefab_stage";
28+
private const string ACTION_CLOSE_PREFAB_STAGE = "close_prefab_stage";
29+
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;
2730

2831
public static object HandleCommand(JObject @params)
2932
{
@@ -50,6 +53,18 @@ public static object HandleCommand(JObject @params)
5053
return GetHierarchy(@params);
5154
case ACTION_MODIFY_CONTENTS:
5255
return ModifyContents(@params);
56+
case ACTION_OPEN_PREFAB_STAGE:
57+
{
58+
string prefabPath = @params["prefabPath"]?.ToString() ?? @params["path"]?.ToString();
59+
return OpenPrefabStage(prefabPath);
60+
}
61+
case ACTION_SAVE_PREFAB_STAGE:
62+
return SavePrefabStage();
63+
case ACTION_CLOSE_PREFAB_STAGE:
64+
{
65+
bool saveBeforeClose = @params["saveBeforeClose"]?.ToObject<bool>() ?? false;
66+
return ClosePrefabStage(saveBeforeClose);
67+
}
5368
default:
5469
return new ErrorResponse($"Unknown action: '{action}'. Valid actions are: {SupportedActions}.");
5570
}
@@ -1261,5 +1276,122 @@ private static void BuildHierarchyItemsRecursive(Transform transform, Transform
12611276
}
12621277

12631278
#endregion
1279+
1280+
#region Prefab Stage
1281+
1282+
private static object OpenPrefabStage(string requestedPath)
1283+
{
1284+
if (string.IsNullOrWhiteSpace(requestedPath))
1285+
{
1286+
return new ErrorResponse("Either 'prefabPath' or 'path' parameter is required for open_prefab_stage.");
1287+
}
1288+
1289+
string sanitizedPath = AssetPathUtility.SanitizeAssetPath(requestedPath);
1290+
if (sanitizedPath == null)
1291+
{
1292+
return new ErrorResponse($"Invalid prefab path (path traversal detected): '{requestedPath}'.");
1293+
}
1294+
1295+
if (!sanitizedPath.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
1296+
{
1297+
return new ErrorResponse($"Prefab path must be within the Assets folder. Got: '{sanitizedPath}'.");
1298+
}
1299+
1300+
if (!sanitizedPath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
1301+
{
1302+
return new ErrorResponse($"Prefab path must end with '.prefab'. Got: '{sanitizedPath}'.");
1303+
}
1304+
1305+
try
1306+
{
1307+
GameObject prefabAsset = AssetDatabase.LoadAssetAtPath<GameObject>(sanitizedPath);
1308+
if (prefabAsset == null)
1309+
{
1310+
return new ErrorResponse($"Prefab asset not found at '{sanitizedPath}'.");
1311+
}
1312+
1313+
var prefabStage = PrefabStageUtility.OpenPrefab(sanitizedPath);
1314+
bool enteredStage = prefabStage != null
1315+
&& string.Equals(prefabStage.assetPath, sanitizedPath, StringComparison.OrdinalIgnoreCase)
1316+
&& prefabStage.prefabContentsRoot != null;
1317+
1318+
if (!enteredStage)
1319+
{
1320+
return new ErrorResponse($"Failed to open prefab stage for '{sanitizedPath}'. PrefabStageUtility.OpenPrefab did not enter the requested prefab stage.");
1321+
}
1322+
1323+
return new SuccessResponse(
1324+
$"Opened prefab stage for '{sanitizedPath}'.",
1325+
new
1326+
{
1327+
prefabPath = sanitizedPath,
1328+
openedPrefabPath = prefabStage.assetPath,
1329+
rootName = prefabStage.prefabContentsRoot.name,
1330+
enteredPrefabStage = enteredStage
1331+
}
1332+
);
1333+
}
1334+
catch (Exception e)
1335+
{
1336+
return new ErrorResponse($"Error opening prefab stage: {e.Message}");
1337+
}
1338+
}
1339+
1340+
private static object SavePrefabStage()
1341+
{
1342+
try
1343+
{
1344+
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
1345+
if (prefabStage == null)
1346+
{
1347+
return new ErrorResponse("Not currently in prefab editing mode. Open a prefab stage first with open_prefab_stage.");
1348+
}
1349+
1350+
string prefabPath = prefabStage.assetPath;
1351+
EditorSceneManager.MarkSceneDirty(prefabStage.scene);
1352+
bool saved = EditorSceneManager.SaveScene(prefabStage.scene);
1353+
if (!saved)
1354+
{
1355+
return new ErrorResponse($"Failed to save prefab stage for '{prefabPath}'. The file may be read-only or the disk may be full.");
1356+
}
1357+
1358+
return new SuccessResponse($"Saved prefab stage changes for '{prefabPath}'.", new { prefabPath, saved });
1359+
}
1360+
catch (Exception e)
1361+
{
1362+
return new ErrorResponse($"Error saving prefab stage: {e.Message}");
1363+
}
1364+
}
1365+
1366+
private static object ClosePrefabStage(bool saveBeforeClose = false)
1367+
{
1368+
try
1369+
{
1370+
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
1371+
if (prefabStage == null)
1372+
{
1373+
return new SuccessResponse("Not currently in prefab editing mode.");
1374+
}
1375+
1376+
if (saveBeforeClose)
1377+
{
1378+
var saveResult = SavePrefabStage();
1379+
if (saveResult is ErrorResponse)
1380+
{
1381+
return saveResult;
1382+
}
1383+
}
1384+
1385+
string prefabPath = prefabStage.assetPath;
1386+
StageUtility.GoToMainStage();
1387+
return new SuccessResponse($"Exited prefab stage for '{prefabPath}'.", new { prefabPath });
1388+
}
1389+
catch (Exception e)
1390+
{
1391+
return new ErrorResponse($"Error closing prefab stage: {e.Message}");
1392+
}
1393+
}
1394+
1395+
#endregion
12641396
}
12651397
}

Server/src/cli/commands/prefab.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def open_stage(path: str):
2929
config = get_config()
3030

3131
params: dict[str, Any] = {
32-
"action": "open_stage",
32+
"action": "open_prefab_stage",
3333
"prefabPath": path,
3434
}
3535

@@ -57,7 +57,7 @@ def close_stage(save: bool):
5757
config = get_config()
5858

5959
params: dict[str, Any] = {
60-
"action": "close_stage",
60+
"action": "close_prefab_stage",
6161
}
6262
if save:
6363
params["saveBeforeClose"] = True
@@ -83,7 +83,7 @@ def save_stage():
8383
"action": "save_prefab_stage",
8484
}
8585

86-
result = run_command("manage_editor", params, config)
86+
result = run_command("manage_prefabs", params, config)
8787
click.echo(format_output(result, config.format))
8888
if result.get("success"):
8989
print_success("Saved prefab stage")

Server/src/services/resources/prefab.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ async def get_prefab_api_docs(_ctx: Context) -> MCPResponse:
5757
"workflow": [
5858
"1. Use manage_asset action=search filterType=Prefab to find prefabs",
5959
"2. Use the asset path to access detailed data via resources below",
60-
"3. Use manage_editor action=open_prefab_stage / save_prefab_stage / close_prefab_stage for prefab editing UI transitions"
60+
"3. Use manage_prefabs action=open_prefab_stage / save_prefab_stage / close_prefab_stage for prefab editing UI transitions"
6161
],
6262
"path_encoding": {
6363
"note": "Prefab paths must be URL-encoded when used in resource URIs",
@@ -80,8 +80,8 @@ async def get_prefab_api_docs(_ctx: Context) -> MCPResponse:
8080
}
8181
},
8282
"related_tools": {
83-
"manage_editor": "Open/save/close prefab stages in the Unity Editor UI",
84-
"manage_prefabs": "Headless prefab inspection and modification without opening prefab stages",
83+
"manage_editor": "Editor controls (play/pause/stop, active tool, tags/layers, package deploy/restore)",
84+
"manage_prefabs": "Prefab stage lifecycle (open/save/close) and headless prefab inspection/modification",
8585
"manage_asset": "Search for prefab assets, get asset info",
8686
"manage_gameobject": "Modify GameObjects in open prefab stage",
8787
"manage_components": "Add/remove/modify components on prefab GameObjects"

0 commit comments

Comments
 (0)