feat: add open_prefab_stage action to manage_editor#968
Conversation
Reviewer's GuideAdds a new manage_editor action open_prefab_stage that opens prefab assets in Unity’s Prefab Stage, wires it through the Python MCP tool with prefab_path/path parameters, updates prefab-related docs, and adds Python and Unity edit-mode tests to validate behavior and error handling. Sequence diagram for manage_editor open_prefab_stage actionsequenceDiagram
actor Client
participant MCPServer
participant ManageEditorTool as ManageEditorTool_manage_editor
participant UnityConnection as UnityConnection_async_send_command_with_retry
participant UnityEditor as ManageEditor_HandleCommand
participant PrefabStage as PrefabStageUtility
Client->>MCPServer: Call manage_editor(action=open_prefab_stage, prefab_path/path)
MCPServer->>ManageEditorTool: Invoke manage_editor
ManageEditorTool->>ManageEditorTool: Validate prefab_path vs path
alt Both set and mismatch
ManageEditorTool-->>Client: {success: False, message: conflict error}
else Valid parameters
ManageEditorTool->>ManageEditorTool: Build params with action and prefabPath/path
ManageEditorTool->>UnityConnection: async_send_command_with_retry(params)
UnityConnection->>UnityEditor: HandleCommand(params)
UnityEditor->>UnityEditor: Read prefabPath = p.Get(prefabPath) ?? p.Get(path)
UnityEditor->>UnityEditor: switch(action)
UnityEditor->>UnityEditor: OpenPrefabStage(prefabPath)
alt Invalid/missing path or load/open failure
UnityEditor-->>UnityConnection: ErrorResponse
else Prefab stage opened
UnityEditor->>PrefabStage: PrefabStageUtility.OpenPrefab(sanitizedPath)
PrefabStage-->>UnityEditor: prefabStage
UnityEditor-->>UnityConnection: SuccessResponse(prefabPath, openedPrefabPath, rootName, enteredPrefabStage)
end
UnityConnection-->>ManageEditorTool: Response dict
ManageEditorTool-->>Client: Response dict
end
Class diagram for ManageEditor with new OpenPrefabStage actionclassDiagram
class ManageEditor {
<<static>>
+object HandleCommand(JObject params)
-static object OpenPrefabStage(string requestedPath)
-static object ClosePrefabStage()
}
class AssetPathUtility {
+static string SanitizeAssetPath(string path)
}
class ErrorResponse {
+ErrorResponse(string message)
}
class SuccessResponse {
+SuccessResponse(string message, object data)
}
class AssetDatabase {
+static GameObject LoadAssetAtPath~GameObject~(string path)
}
class PrefabStageUtility {
+static PrefabStage OpenPrefab(string assetPath)
}
class PrefabStage {
+string assetPath
+GameObject prefabContentsRoot
}
class GameObject {
+string name
}
ManageEditor --> AssetPathUtility : uses
ManageEditor --> ErrorResponse : returns
ManageEditor --> SuccessResponse : returns
ManageEditor --> AssetDatabase : uses
ManageEditor --> PrefabStageUtility : uses
PrefabStageUtility --> PrefabStage : creates
PrefabStage --> GameObject : has prefabContentsRoot
SuccessResponse --> GameObject : includes rootName in data
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
📝 WalkthroughWalkthroughThis PR adds support for opening prefab editing stages directly through the MCP editor tooling interface. It introduces a new Changes
Sequence DiagramsequenceDiagram
participant Client as MCP Client
participant Server as manage_editor Service
participant Unity as ManageEditor.HandleCommand
participant Util as PrefabStageUtility
Client->>Server: open_prefab_stage<br/>(prefab_path="Assets/...")
Server->>Server: Validate parameters<br/>(ensure single path provided)
Server->>Server: Construct params<br/>(map to prefabPath)
Server->>Unity: Forward command<br/>action + params
Unity->>Unity: OpenPrefabStage()<br/>Sanitize & validate path
Unity->>Util: OpenPrefab(sanitizedPath)
Util->>Util: Load asset & enter stage
Util-->>Unity: Stage created
Unity->>Unity: Verify stage entry<br/>(assetPath, prefabContentsRoot)
alt Success
Unity-->>Server: Success response<br/>(stage/root details)
Server-->>Client: Success payload
else Failure
Unity-->>Server: ErrorResponse
Server-->>Client: Error details
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
Adds a new manage_editor action for opening Unity Prefab Stage, wiring it through the Python MCP tool surface and updating prefab documentation and tests to reflect the new UI transition flow.
Changes:
- Add
open_prefab_stagehandling to UnityManageEditorplus prefab-path validation and structured response data. - Expose
prefab_path(andpathalias) on the Pythonmanage_editortool and add Python + Unity edit-mode tests. - Update prefab resource docs and tools reference to direct prefab-stage UI transitions through
manage_editor.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| unity-mcp-skill/references/tools-reference.md | Documents the new open_prefab_stage usage example. |
| TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageEditorPrefabStageTests.cs.meta | Adds Unity meta file for the new edit-mode test. |
| TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageEditorPrefabStageTests.cs | Adds Unity edit-mode coverage for open/close prefab stage and alias behavior. |
| Server/tests/test_manage_editor.py | Adds Python tests for tool surface parameters, description, forwarding, and conflict handling. |
| Server/src/services/tools/manage_editor.py | Adds open_prefab_stage action + prefab_path/path parameter forwarding. |
| Server/src/services/resources/prefab.py | Updates prefab API docs to route prefab-stage UI transitions via manage_editor. |
| MCPForUnity/Editor/Tools/ManageEditor.cs | Implements open_prefab_stage action with path sanitization and stage-opening logic. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| { | ||
| if (string.IsNullOrWhiteSpace(requestedPath)) | ||
| { | ||
| return new ErrorResponse("'prefabPath' parameter is required for open_prefab_stage."); |
There was a problem hiding this comment.
open_prefab_stage now accepts both 'prefabPath' and the compatibility alias 'path' (see HandleCommand extracting prefabPath = p.Get("prefabPath") ?? p.Get("path")), but the validation error still says only "'prefabPath' parameter is required". This is misleading for callers using the alias; update the message to mention both accepted keys (e.g., prefabPath or path).
| return new ErrorResponse("'prefabPath' parameter is required for open_prefab_stage."); | |
| return new ErrorResponse("Either 'prefabPath' or 'path' parameter is required for open_prefab_stage."); |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@MCPForUnity/Editor/Tools/ManageEditor.cs`:
- Around line 47-50: Before collapsing aliases, validate that callers do not
supply conflicting prefab/path values: read both p.Get("prefabPath") and
p.Get("path") into temporaries (treat empty string as missing via
string.IsNullOrEmpty), and if both are present (non-empty) reject/return an
error the same way the Python service does; only after this guard set prefabPath
= non-empty prefabPath if present else path. Reference the variables prefabPath,
p.Get("prefabPath"), p.Get("path") and the handler method in ManageEditor.cs
(e.g., the OpenPrefabStage dispatch) to locate where to add the guard.
In `@Server/tests/test_manage_editor.py`:
- Around line 150-161: The test
test_open_prefab_stage_rejects_conflicting_path_inputs must also assert that no
Unity command was sent: ensure mock_unity (the fixture) did not have
send_with_unity_instance invoked when manage_editor is called with both
prefab_path and path; after calling manage_editor, add an assertion that
mock_unity.send_with_unity_instance was not called (or use
mock_unity.assert_not_called()/mock_unity.send_with_unity_instance.assert_not_called())
to guarantee the failure occurs before dispatch to Unity.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: c064c07a-b5b5-40a9-bf69-d6ba21752ce7
📒 Files selected for processing (7)
MCPForUnity/Editor/Tools/ManageEditor.csServer/src/services/resources/prefab.pyServer/src/services/tools/manage_editor.pyServer/tests/test_manage_editor.pyTestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageEditorPrefabStageTests.csTestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManageEditorPrefabStageTests.cs.metaunity-mcp-skill/references/tools-reference.md
| // Parameters for specific actions | ||
| string tagName = p.Get("tagName"); | ||
| string layerName = p.Get("layerName"); | ||
| string prefabPath = p.Get("prefabPath") ?? p.Get("path"); |
There was a problem hiding this comment.
Reject mismatched prefabPath/path in the Unity handler too.
Line 50 collapses the aliases before validation, so a direct manage_editor caller that sends both values will silently prefer prefabPath instead of failing. It also lets prefabPath="" mask a valid path alias because ?? only falls back on null. The Python surface already rejects conflicting values in Server/src/services/tools/manage_editor.py at Lines 44-48, so the editor layer should enforce the same contract. If you take this route, the new OpenPrefabStage_PrefabPathTakesPrecedenceOverPath test should flip to a rejection case as well.
🛡️ Add the guard before dispatch
- string prefabPath = p.Get("prefabPath") ?? p.Get("path");
+ string prefabPathParam = p.Get("prefabPath");
+ string pathParam = p.Get("path");
+ if (
+ !string.IsNullOrWhiteSpace(prefabPathParam)
+ && !string.IsNullOrWhiteSpace(pathParam)
+ && !string.Equals(prefabPathParam, pathParam, StringComparison.Ordinal)
+ )
+ {
+ return new ErrorResponse("Provide only one of 'prefabPath' or 'path', or ensure both match.");
+ }
+ string prefabPath = !string.IsNullOrWhiteSpace(prefabPathParam)
+ ? prefabPathParam
+ : pathParam;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@MCPForUnity/Editor/Tools/ManageEditor.cs` around lines 47 - 50, Before
collapsing aliases, validate that callers do not supply conflicting prefab/path
values: read both p.Get("prefabPath") and p.Get("path") into temporaries (treat
empty string as missing via string.IsNullOrEmpty), and if both are present
(non-empty) reject/return an error the same way the Python service does; only
after this guard set prefabPath = non-empty prefabPath if present else path.
Reference the variables prefabPath, p.Get("prefabPath"), p.Get("path") and the
handler method in ManageEditor.cs (e.g., the OpenPrefabStage dispatch) to locate
where to add the guard.
| def test_open_prefab_stage_rejects_conflicting_path_inputs(mock_unity): | ||
| """Conflicting aliases should fail fast before sending a Unity command.""" | ||
| result = asyncio.run( | ||
| manage_editor( | ||
| SimpleNamespace(), | ||
| action="open_prefab_stage", | ||
| prefab_path="Assets/Prefabs/Primary.prefab", | ||
| path="Assets/Prefabs/Alias.prefab", | ||
| ) | ||
| ) | ||
| assert result["success"] is False | ||
| assert "Provide only one of prefab_path or path" in result.get("message", "") |
There was a problem hiding this comment.
Assert that the conflicting alias case never reaches Unity.
This test verifies the error message, but not the promised "fail before dispatch" behavior. Without checking mock_unity, it would still pass if send_with_unity_instance were called and the failure were synthesized afterward.
🧪 Tighten the expectation
def test_open_prefab_stage_rejects_conflicting_path_inputs(mock_unity):
"""Conflicting aliases should fail fast before sending a Unity command."""
result = asyncio.run(
manage_editor(
SimpleNamespace(),
action="open_prefab_stage",
prefab_path="Assets/Prefabs/Primary.prefab",
path="Assets/Prefabs/Alias.prefab",
)
)
assert result["success"] is False
assert "Provide only one of prefab_path or path" in result.get("message", "")
+ assert "params" not in mock_unity📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def test_open_prefab_stage_rejects_conflicting_path_inputs(mock_unity): | |
| """Conflicting aliases should fail fast before sending a Unity command.""" | |
| result = asyncio.run( | |
| manage_editor( | |
| SimpleNamespace(), | |
| action="open_prefab_stage", | |
| prefab_path="Assets/Prefabs/Primary.prefab", | |
| path="Assets/Prefabs/Alias.prefab", | |
| ) | |
| ) | |
| assert result["success"] is False | |
| assert "Provide only one of prefab_path or path" in result.get("message", "") | |
| def test_open_prefab_stage_rejects_conflicting_path_inputs(mock_unity): | |
| """Conflicting aliases should fail fast before sending a Unity command.""" | |
| result = asyncio.run( | |
| manage_editor( | |
| SimpleNamespace(), | |
| action="open_prefab_stage", | |
| prefab_path="Assets/Prefabs/Primary.prefab", | |
| path="Assets/Prefabs/Alias.prefab", | |
| ) | |
| ) | |
| assert result["success"] is False | |
| assert "Provide only one of prefab_path or path" in result.get("message", "") | |
| assert "params" not in mock_unity |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Server/tests/test_manage_editor.py` around lines 150 - 161, The test
test_open_prefab_stage_rejects_conflicting_path_inputs must also assert that no
Unity command was sent: ensure mock_unity (the fixture) did not have
send_with_unity_instance invoked when manage_editor is called with both
prefab_path and path; after calling manage_editor, add an assertion that
mock_unity.send_with_unity_instance was not called (or use
mock_unity.assert_not_called()/mock_unity.send_with_unity_instance.assert_not_called())
to guarantee the failure occurs before dispatch to Unity.
Adds save_prefab_stage to manage_editor to complete the prefab stage workflow alongside the existing open_prefab_stage and close_prefab_stage. - C#: SavePrefabStage() uses EditorSceneManager.MarkSceneDirty + SaveScene on the prefab stage scene, returns ErrorResponse when no stage is open or save fails - Python: adds save_prefab_stage to action Literal and tool description - Tests: 3 new tests covering forwarding, description, and clean params open_prefab_stage was already merged in CoplayDev#968. This PR only adds save_prefab_stage.
Summary
Rebased version of #947 (by @jiajunfeng) onto current beta, resolving merge conflicts with undo/redo additions.
manage_editor(action="open_prefab_stage")to open a prefab asset in Prefab Stageprefab_pathandpath(alias) parametersmanage_editorChanges from original PR
Test plan
cd Server && uv run pytest tests/test_manage_editor.py -vManageEditorPrefabStageTestsmanage_editor(action="open_prefab_stage", prefab_path="Assets/Prefabs/SomePrefab.prefab")Credit: Original implementation by @jiajunfeng (#947)
Summary by Sourcery
Add support for opening Unity prefab stages via the manage_editor tool and wire it through the Unity editor backend and Python MCP surface.
New Features:
Enhancements:
Tests:
Summary by CodeRabbit
Release Notes
New Features
Documentation
Tests