Skip to content
Open
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
30 changes: 29 additions & 1 deletion MCPForUnity/Editor/Tools/ManageEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ public static object HandleCommand(JObject @params)
// Prefab Stage
case "open_prefab_stage":
return OpenPrefabStage(prefabPath);
case "save_prefab_stage":
return SavePrefabStage();
case "close_prefab_stage":
return ClosePrefabStage();

Expand Down Expand Up @@ -180,7 +182,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, 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, 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."
);
}
}
Expand Down Expand Up @@ -460,6 +462,32 @@ private static object OpenPrefabStage(string requestedPath)
}
}

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
Expand Down
4 changes: 2 additions & 2 deletions Server/src/services/tools/manage_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
from transport.legacy.unity_connection import async_send_command_with_retry

@mcp_for_unity_tool(
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.",
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.",
annotations=ToolAnnotations(
title="Manage Editor",
),
)
async def manage_editor(
ctx: Context,
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."],
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."],
tool_name: Annotated[str,
"Tool name when setting active tool"] | None = None,
tag_name: Annotated[str,
Expand Down
34 changes: 33 additions & 1 deletion Server/tests/test_manage_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def test_redo_forwards_to_unity(mock_unity):
UNITY_FORWARDED_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",
"open_prefab_stage", "save_prefab_stage", "close_prefab_stage", "deploy_package", "restore_package",
"undo", "redo",
]

Expand Down Expand Up @@ -159,3 +159,35 @@ def test_open_prefab_stage_rejects_conflicting_path_inputs(mock_unity):
)
assert result["success"] is False
assert "Provide only one of prefab_path or path" in result.get("message", "")


# ── save_prefab_stage ────────────────────────────────────────────────


def test_manage_editor_description_mentions_save_prefab_stage():
"""The tool description should advertise the save_prefab_stage action."""
editor_tool = next(
(t for t in get_registered_tools() if t["name"] == "manage_editor"), None
)
assert editor_tool is not None
desc = editor_tool.get("description") or editor_tool.get("kwargs", {}).get("description", "")
assert "save_prefab_stage" in desc


def test_save_prefab_stage_forwards_to_unity(mock_unity):
"""save_prefab_stage should forward to Unity without extra parameters."""
result = asyncio.run(manage_editor(SimpleNamespace(), action="save_prefab_stage"))
assert result["success"] is True
assert mock_unity["params"]["action"] == "save_prefab_stage"
assert mock_unity["tool_name"] == "manage_editor"


def test_save_prefab_stage_omits_none_params(mock_unity):
"""save_prefab_stage should not include toolName, tagName, layerName, or path params."""
asyncio.run(manage_editor(SimpleNamespace(), action="save_prefab_stage"))
params = mock_unity["params"]
assert "toolName" not in params
assert "tagName" not in params
assert "layerName" not in params
assert "prefabPath" not in params
assert "path" not in params