Skip to content

Commit fd8a6da

Browse files
committed
Update docs and CLI usage
1 parent 99ba2ad commit fd8a6da

7 files changed

Lines changed: 222 additions & 8 deletions

File tree

Server/src/cli/CLI_USAGE_GUIDE.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,15 @@ unity-mcp prefab save
560560

561561
# Close prefab stage
562562
unity-mcp prefab close
563+
564+
# Modify prefab contents (headless)
565+
unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --target Weapon --position "0,1,2"
566+
unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --delete-child Child1 --delete-child "Turret/Barrel"
567+
unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --set-property "Rigidbody.mass=5"
568+
unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --add-component BoxCollider --remove-component SphereCollider
569+
unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --create-child '{"name":"Spawn","primitive_type":"Sphere"}'
570+
unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --name NewName --tag Player --layer UI
571+
unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --inactive
563572
```
564573

565574
### UI Commands

Server/src/cli/commands/prefab.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,10 @@ def _parse_vector3(value: str) -> list[float]:
254254
parts = value.split(",")
255255
if len(parts) != 3:
256256
raise click.BadParameter("Must be 'x,y,z' format")
257-
return [float(p.strip()) for p in parts]
257+
try:
258+
return [float(p.strip()) for p in parts]
259+
except ValueError:
260+
raise click.BadParameter(f"All components must be numeric, got: '{value}'")
258261

259262

260263
def _parse_property(prop_str: str) -> tuple[str, str, Any]:
@@ -362,7 +365,7 @@ def modify(path: str, target: Optional[str], position: Optional[str], rotation:
362365
try:
363366
params["createChild"] = json.loads(create_child)
364367
except json.JSONDecodeError as e:
365-
raise click.BadParameter(f"Invalid JSON for --create-child: {e}")
368+
raise click.BadParameter(f"Invalid JSON for --create-child: {e}") from e
366369

367370
result = run_command("manage_prefabs", params, config)
368371
click.echo(format_output(result, config.format))

Server/tests/test_cli.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,77 @@ def test_prefab_modify_active_state(self, runner, mock_unity_response):
745745
params = call_args[1]
746746
assert params["setActive"] is False
747747

748+
def test_prefab_modify_active_flag(self, runner, mock_unity_response):
749+
"""Test prefab modify --active flag."""
750+
with patch("cli.commands.prefab.run_command", return_value=mock_unity_response) as mock_run:
751+
result = runner.invoke(cli, [
752+
"prefab", "modify", "Assets/Prefabs/Player.prefab",
753+
"--active"
754+
])
755+
assert result.exit_code == 0
756+
call_args = mock_run.call_args[0]
757+
params = call_args[1]
758+
assert params["setActive"] is True
759+
760+
def test_prefab_modify_name_tag_layer_parent(self, runner, mock_unity_response):
761+
"""Test prefab modify with name, tag, layer, and parent options."""
762+
with patch("cli.commands.prefab.run_command", return_value=mock_unity_response) as mock_run:
763+
result = runner.invoke(cli, [
764+
"prefab", "modify", "Assets/Prefabs/Player.prefab",
765+
"--target", "Child1",
766+
"--name", "RenamedChild",
767+
"--tag", "Player",
768+
"--layer", "UI",
769+
"--parent", "NewParent"
770+
])
771+
assert result.exit_code == 0
772+
call_args = mock_run.call_args[0]
773+
params = call_args[1]
774+
assert params["target"] == "Child1"
775+
assert params["name"] == "RenamedChild"
776+
assert params["tag"] == "Player"
777+
assert params["layer"] == "UI"
778+
assert params["parent"] == "NewParent"
779+
780+
def test_prefab_modify_invalid_vector_non_numeric(self, runner, mock_unity_response):
781+
"""Test prefab modify rejects non-numeric vector components."""
782+
result = runner.invoke(cli, [
783+
"prefab", "modify", "Assets/Prefabs/Player.prefab",
784+
"--position", "1,foo,3"
785+
])
786+
assert result.exit_code != 0
787+
788+
def test_prefab_modify_invalid_vector_wrong_count(self, runner, mock_unity_response):
789+
"""Test prefab modify rejects vectors with wrong component count."""
790+
result = runner.invoke(cli, [
791+
"prefab", "modify", "Assets/Prefabs/Player.prefab",
792+
"--position", "1,2"
793+
])
794+
assert result.exit_code != 0
795+
796+
def test_prefab_modify_set_property_string_value(self, runner, mock_unity_response):
797+
"""Test prefab modify set-property with string values."""
798+
with patch("cli.commands.prefab.run_command", return_value=mock_unity_response) as mock_run:
799+
result = runner.invoke(cli, [
800+
"prefab", "modify", "Assets/Prefabs/Player.prefab",
801+
"--set-property", "MyScript.label=hello world"
802+
])
803+
assert result.exit_code == 0
804+
call_args = mock_run.call_args[0]
805+
params = call_args[1]
806+
assert params["componentProperties"]["MyScript"]["label"] == "hello world"
807+
808+
def test_prefab_modify_no_options_sends_minimal_params(self, runner, mock_unity_response):
809+
"""Test prefab modify with no options sends only action and prefabPath."""
810+
with patch("cli.commands.prefab.run_command", return_value=mock_unity_response) as mock_run:
811+
result = runner.invoke(cli, [
812+
"prefab", "modify", "Assets/Prefabs/Player.prefab"
813+
])
814+
assert result.exit_code == 0
815+
call_args = mock_run.call_args[0]
816+
params = call_args[1]
817+
assert params == {"action": "modify_contents", "prefabPath": "Assets/Prefabs/Player.prefab"}
818+
748819

749820
# =============================================================================
750821
# Material Command Tests

Server/tests/test_manage_prefabs.py

Lines changed: 104 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,45 @@
1-
"""Tests for manage_prefabs tool - component_properties parameter."""
1+
"""Tests for manage_prefabs tool."""
22

3+
import asyncio
34
import inspect
5+
from types import SimpleNamespace
6+
from unittest.mock import AsyncMock
7+
8+
import pytest
49

510
from services.tools.manage_prefabs import manage_prefabs
11+
from services.registry import get_registered_tools
12+
13+
14+
# ── Fixture ──────────────────────────────────────────────────────────
15+
16+
17+
@pytest.fixture
18+
def mock_unity(monkeypatch):
19+
captured: dict[str, object] = {}
20+
21+
async def fake_send(send_fn, unity_instance, tool_name, params):
22+
captured["unity_instance"] = unity_instance
23+
captured["tool_name"] = tool_name
24+
captured["params"] = params
25+
return {"success": True, "message": "ok"}
26+
27+
monkeypatch.setattr(
28+
"services.tools.manage_prefabs.get_unity_instance_from_context",
29+
AsyncMock(return_value="unity-instance-1"),
30+
)
31+
monkeypatch.setattr(
32+
"services.tools.manage_prefabs.send_with_unity_instance",
33+
fake_send,
34+
)
35+
monkeypatch.setattr(
36+
"services.tools.manage_prefabs.preflight",
37+
AsyncMock(return_value=None),
38+
)
39+
return captured
40+
41+
42+
# ── component_properties ─────────────────────────────────────────────
643

744

845
class TestManagePrefabsComponentProperties:
@@ -21,13 +58,10 @@ def test_component_properties_parameter_is_optional(self):
2158

2259
def test_tool_description_mentions_component_properties(self):
2360
"""The tool description should mention component_properties."""
24-
from services.registry import get_registered_tools
25-
tools = get_registered_tools()
2661
prefab_tool = next(
27-
(t for t in tools if t["name"] == "manage_prefabs"), None
62+
(t for t in get_registered_tools() if t["name"] == "manage_prefabs"), None
2863
)
2964
assert prefab_tool is not None
30-
# Description is stored at top level or in kwargs depending on how the decorator stores it
3165
desc = prefab_tool.get("description") or prefab_tool.get("kwargs", {}).get("description", "")
3266
assert "component_properties" in desc
3367

@@ -36,3 +70,68 @@ def test_required_params_include_modify_contents(self):
3670
from services.tools.manage_prefabs import REQUIRED_PARAMS
3771
assert "modify_contents" in REQUIRED_PARAMS
3872
assert "prefab_path" in REQUIRED_PARAMS["modify_contents"]
73+
74+
75+
# ── delete_child ─────────────────────────────────────────────────────
76+
77+
78+
class TestManagePrefabsDeleteChild:
79+
"""Tests for the delete_child parameter on manage_prefabs."""
80+
81+
def test_delete_child_parameter_exists(self):
82+
"""The manage_prefabs tool should have a delete_child parameter."""
83+
sig = inspect.signature(manage_prefabs)
84+
assert "delete_child" in sig.parameters
85+
86+
def test_delete_child_parameter_is_optional(self):
87+
"""delete_child should default to None."""
88+
sig = inspect.signature(manage_prefabs)
89+
param = sig.parameters["delete_child"]
90+
assert param.default is None
91+
92+
def test_tool_description_mentions_delete_child(self):
93+
"""The tool description should mention delete_child."""
94+
prefab_tool = next(
95+
(t for t in get_registered_tools() if t["name"] == "manage_prefabs"), None
96+
)
97+
assert prefab_tool is not None
98+
desc = prefab_tool.get("description") or prefab_tool.get("kwargs", {}).get("description", "")
99+
assert "delete_child" in desc
100+
101+
def test_delete_child_string_forwards_to_unity(self, mock_unity):
102+
"""A single string delete_child should be forwarded as-is."""
103+
result = asyncio.run(
104+
manage_prefabs(
105+
SimpleNamespace(),
106+
action="modify_contents",
107+
prefab_path="Assets/Prefabs/Test.prefab",
108+
delete_child="Child1",
109+
)
110+
)
111+
assert result["success"] is True
112+
assert mock_unity["tool_name"] == "manage_prefabs"
113+
assert mock_unity["params"]["deleteChild"] == "Child1"
114+
115+
def test_delete_child_list_forwards_to_unity(self, mock_unity):
116+
"""A list of delete_child paths should be forwarded as-is."""
117+
result = asyncio.run(
118+
manage_prefabs(
119+
SimpleNamespace(),
120+
action="modify_contents",
121+
prefab_path="Assets/Prefabs/Test.prefab",
122+
delete_child=["Child1", "Child2/Grandchild"],
123+
)
124+
)
125+
assert result["success"] is True
126+
assert mock_unity["params"]["deleteChild"] == ["Child1", "Child2/Grandchild"]
127+
128+
def test_delete_child_none_omitted_from_params(self, mock_unity):
129+
"""When delete_child is None, deleteChild should not appear in params."""
130+
asyncio.run(
131+
manage_prefabs(
132+
SimpleNamespace(),
133+
action="modify_contents",
134+
prefab_path="Assets/Prefabs/Test.prefab",
135+
)
136+
)
137+
assert "deleteChild" not in mock_unity["params"]

docs/guides/CLI_EXAMPLE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,9 @@ unity-mcp prefab open "Assets/Prefabs/File.prefab"
120120
unity-mcp prefab save
121121
unity-mcp prefab close
122122
unity-mcp prefab create "GameObject" --path "Assets/Prefabs"
123+
unity-mcp prefab modify "Assets/Prefabs/File.prefab" --delete-child Child1
124+
unity-mcp prefab modify "Assets/Prefabs/File.prefab" --target Weapon --position "0,1,2"
125+
unity-mcp prefab modify "Assets/Prefabs/File.prefab" --set-property "Rigidbody.mass=5"
123126
```
124127

125128
**Material Operations**

docs/guides/CLI_USAGE.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,13 @@ unity-mcp prefab close
298298

299299
# Create from GameObject
300300
unity-mcp prefab create "Player" --path "Assets/Prefabs"
301+
302+
# Modify prefab contents (headless, no UI)
303+
unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --target Weapon --position "0,1,2"
304+
unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --delete-child Child1 --delete-child "Turret/Barrel"
305+
unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --set-property "Rigidbody.mass=5"
306+
unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --add-component BoxCollider
307+
unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --create-child '{"name":"Spawn","primitive_type":"Sphere"}'
301308
```
302309

303310
### Asset Operations
@@ -487,7 +494,7 @@ unity-mcp raw manage_packages '{"action": "list_packages"}'
487494
| `shader` | `create`, `read`, `update`, `delete` |
488495
| `editor` | `play`, `pause`, `stop`, `refresh`, `console`, `menu`, `tool`, `add-tag`, `remove-tag`, `add-layer`, `remove-layer`, `tests`, `poll-test`, `custom-tool` |
489496
| `asset` | `search`, `info`, `create`, `delete`, `duplicate`, `move`, `rename`, `import`, `mkdir` |
490-
| `prefab` | `open`, `close`, `save`, `create` |
497+
| `prefab` | `open`, `close`, `save`, `create`, `modify` |
491498
| `material` | `info`, `create`, `set-color`, `set-property`, `assign`, `set-renderer-color` |
492499
| `camera` | `ping`, `list`, `create`, `set-target`, `set-lens`, `set-priority`, `set-body`, `set-aim`, `set-noise`, `add-extension`, `remove-extension`, `ensure-brain`, `brain-status`, `set-blend`, `force`, `release`, `screenshot`, `screenshot-multiview` |
493500
| `graphics` | `ping`, `volume-create`, `volume-add-effect`, `volume-set-effect`, `volume-remove-effect`, `volume-info`, `volume-set-properties`, `volume-list-effects`, `volume-create-profile`, `pipeline-info`, `pipeline-settings`, `pipeline-set-quality`, `pipeline-set-settings`, `bake-start`, `bake-cancel`, `bake-status`, `bake-clear`, `bake-settings`, `bake-set-settings`, `bake-reflection-probe`, `bake-create-probes`, `bake-create-reflection`, `stats`, `stats-memory`, `stats-debug-mode`, `feature-list`, `feature-add`, `feature-remove`, `feature-configure`, `feature-reorder`, `feature-toggle`, `skybox-info`, `skybox-set-material`, `skybox-set-properties`, `skybox-set-ambient`, `skybox-set-fog`, `skybox-set-reflection`, `skybox-set-sun` |

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,28 @@ manage_prefabs(
523523
position=[0, 1, 0],
524524
components_to_add=["AudioSource"]
525525
)
526+
527+
# Delete child GameObjects from prefab
528+
manage_prefabs(
529+
action="modify_contents",
530+
prefab_path="Assets/Prefabs/Player.prefab",
531+
delete_child=["OldChild", "Turret/Barrel"] # single string or list
532+
)
533+
534+
# Create child GameObject in prefab
535+
manage_prefabs(
536+
action="modify_contents",
537+
prefab_path="Assets/Prefabs/Player.prefab",
538+
create_child={"name": "SpawnPoint", "primitive_type": "Sphere", "position": [0, 2, 0]}
539+
)
540+
541+
# Set component properties on prefab contents
542+
manage_prefabs(
543+
action="modify_contents",
544+
prefab_path="Assets/Prefabs/Player.prefab",
545+
target="ChildObject",
546+
component_properties={"Rigidbody": {"mass": 5.0}, "MyScript": {"health": 100}}
547+
)
526548
```
527549

528550
---

0 commit comments

Comments
 (0)