Skip to content

Commit e2903a7

Browse files
authored
Merge pull request #990 from zaferdace/feat/save-prefab-stage
feat(manage_editor): add save_prefab_stage action
2 parents cf55776 + 0dd22dc commit e2903a7

6 files changed

Lines changed: 71 additions & 18 deletions

File tree

MCPForUnity/Editor/Tools/ManageEditor.cs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ public static object HandleCommand(JObject @params)
140140
// Prefab Stage
141141
case "open_prefab_stage":
142142
return OpenPrefabStage(prefabPath);
143+
case "save_prefab_stage":
144+
return SavePrefabStage();
143145
case "close_prefab_stage":
144146
return ClosePrefabStage();
145147

@@ -180,7 +182,7 @@ public static object HandleCommand(JObject @params)
180182

181183
default:
182184
return new ErrorResponse(
183-
$"Unknown action: '{action}'. Supported actions: play, pause, stop, set_active_tool, add_tag, remove_tag, add_layer, remove_layer, open_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."
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."
184186
);
185187
}
186188
}
@@ -460,6 +462,32 @@ private static object OpenPrefabStage(string requestedPath)
460462
}
461463
}
462464

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+
463491
private static object ClosePrefabStage()
464492
{
465493
try

Server/src/cli/commands/prefab.py

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -69,32 +69,24 @@ def close_stage(save: bool):
6969

7070

7171
@prefab.command("save")
72-
@click.option(
73-
"--force", "-f",
74-
is_flag=True,
75-
help="Force save even if no changes detected. Useful for automated workflows."
76-
)
7772
@handle_unity_errors
78-
def save_stage(force: bool):
73+
def save_stage():
7974
"""Save the currently open prefab stage.
8075
8176
\b
8277
Examples:
8378
unity-mcp prefab save
84-
unity-mcp prefab save --force
8579
"""
8680
config = get_config()
8781

8882
params: dict[str, Any] = {
89-
"action": "save_open_stage",
83+
"action": "save_prefab_stage",
9084
}
91-
if force:
92-
params["force"] = True
9385

94-
result = run_command("manage_prefabs", params, config)
86+
result = run_command("manage_editor", params, config)
9587
click.echo(format_output(result, config.format))
9688
if result.get("success"):
97-
print_success("Saved prefab")
89+
print_success("Saved prefab stage")
9890

9991

10092
@prefab.command("info")

Server/src/services/resources/prefab.py

Lines changed: 2 additions & 2 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 / close_prefab_stage for prefab editing UI transitions"
60+
"3. Use manage_editor 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,7 +80,7 @@ async def get_prefab_api_docs(_ctx: Context) -> MCPResponse:
8080
}
8181
},
8282
"related_tools": {
83-
"manage_editor": "Open/close prefab stages in the Unity Editor UI",
83+
"manage_editor": "Open/save/close prefab stages in the Unity Editor UI",
8484
"manage_prefabs": "Headless prefab inspection and modification without opening prefab stages",
8585
"manage_asset": "Search for prefab assets, get asset info",
8686
"manage_gameobject": "Modify GameObjects in open prefab stage",

Server/src/services/tools/manage_editor.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@
1010
from transport.legacy.unity_connection import async_send_command_with_retry
1111

1212
@mcp_for_unity_tool(
13-
description="Controls and queries the Unity editor's state and settings. Read-only actions: telemetry_status, telemetry_ping. Modifying actions: play, pause, stop, set_active_tool, add_tag, remove_tag, add_layer, remove_layer, open_prefab_stage, close_prefab_stage, deploy_package, restore_package, undo, redo. open_prefab_stage opens a prefab asset in Unity's prefab editing mode. deploy_package copies the configured MCPForUnity source folder into the project's installed package location (triggers recompile, no confirmation dialog). restore_package reverts to the pre-deployment backup. undo/redo perform Unity editor undo/redo and return the affected group name.",
13+
description="Controls and queries the Unity editor's state and settings. Read-only actions: telemetry_status, telemetry_ping. Modifying 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. open_prefab_stage opens a prefab asset in Unity's prefab editing mode. save_prefab_stage saves changes in the currently open prefab stage back to the prefab asset. deploy_package copies the configured MCPForUnity source folder into the project's installed package location (triggers recompile, no confirmation dialog). restore_package reverts to the pre-deployment backup. undo/redo perform Unity editor undo/redo and return the affected group name.",
1414
annotations=ToolAnnotations(
1515
title="Manage Editor",
1616
),
1717
)
1818
async def manage_editor(
1919
ctx: Context,
20-
action: Annotated[Literal["telemetry_status", "telemetry_ping", "play", "pause", "stop", "set_active_tool", "add_tag", "remove_tag", "add_layer", "remove_layer", "open_prefab_stage", "close_prefab_stage", "deploy_package", "restore_package", "undo", "redo"], "Get and update the Unity Editor state. open_prefab_stage opens a prefab asset in prefab editing mode; close_prefab_stage exits prefab editing mode and returns to the main scene stage. deploy_package copies the configured MCPForUnity source into the project's package location (triggers recompile). restore_package reverts the last deployment from backup. undo/redo perform editor undo/redo."],
20+
action: Annotated[Literal["telemetry_status", "telemetry_ping", "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"], "Get and update the Unity Editor state. open_prefab_stage opens a prefab asset in prefab editing mode; save_prefab_stage saves changes in the open prefab stage back to the asset; close_prefab_stage exits prefab editing mode and returns to the main scene stage. deploy_package copies the configured MCPForUnity source into the project's package location (triggers recompile). restore_package reverts the last deployment from backup. undo/redo perform editor undo/redo."],
2121
tool_name: Annotated[str,
2222
"Tool name when setting active tool"] | None = None,
2323
tag_name: Annotated[str,

Server/tests/test_manage_editor.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def test_redo_forwards_to_unity(mock_unity):
5555
UNITY_FORWARDED_ACTIONS = [
5656
"play", "pause", "stop", "set_active_tool",
5757
"add_tag", "remove_tag", "add_layer", "remove_layer",
58-
"open_prefab_stage", "close_prefab_stage", "deploy_package", "restore_package",
58+
"open_prefab_stage", "save_prefab_stage", "close_prefab_stage", "deploy_package", "restore_package",
5959
"undo", "redo",
6060
]
6161

@@ -159,3 +159,35 @@ def test_open_prefab_stage_rejects_conflicting_path_inputs(mock_unity):
159159
)
160160
assert result["success"] is False
161161
assert "Provide only one of prefab_path or path" in result.get("message", "")
162+
163+
164+
# ── save_prefab_stage ────────────────────────────────────────────────
165+
166+
167+
def test_manage_editor_description_mentions_save_prefab_stage():
168+
"""The tool description should advertise the save_prefab_stage action."""
169+
editor_tool = next(
170+
(t for t in get_registered_tools() if t["name"] == "manage_editor"), None
171+
)
172+
assert editor_tool is not None
173+
desc = editor_tool.get("description") or editor_tool.get("kwargs", {}).get("description", "")
174+
assert "save_prefab_stage" in desc
175+
176+
177+
def test_save_prefab_stage_forwards_to_unity(mock_unity):
178+
"""save_prefab_stage should forward to Unity without extra parameters."""
179+
result = asyncio.run(manage_editor(SimpleNamespace(), action="save_prefab_stage"))
180+
assert result["success"] is True
181+
assert mock_unity["params"]["action"] == "save_prefab_stage"
182+
assert mock_unity["tool_name"] == "manage_editor"
183+
184+
185+
def test_save_prefab_stage_omits_none_params(mock_unity):
186+
"""save_prefab_stage should not include toolName, tagName, layerName, or path params."""
187+
asyncio.run(manage_editor(SimpleNamespace(), action="save_prefab_stage"))
188+
params = mock_unity["params"]
189+
assert "toolName" not in params
190+
assert "tagName" not in params
191+
assert "layerName" not in params
192+
assert "prefabPath" not in params
193+
assert "path" not in params

unity-mcp-skill/references/tools-reference.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,7 @@ manage_editor(action="add_layer", layer_name="Projectiles")
730730
manage_editor(action="remove_layer", layer_name="OldLayer")
731731

732732
manage_editor(action="open_prefab_stage", prefab_path="Assets/Prefabs/Enemy.prefab")
733+
manage_editor(action="save_prefab_stage") # Save changes in the open prefab stage
733734
manage_editor(action="close_prefab_stage") # Exit prefab editing mode back to main scene
734735

735736
# Package deployment (no confirmation dialog — designed for LLM-driven iteration)

0 commit comments

Comments
 (0)