diff --git a/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs b/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs
index b24eee044..bedd4a36d 100644
--- a/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs
+++ b/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs
@@ -905,6 +905,21 @@ private static (bool modified, ErrorResponse error) ApplyModificationsToPrefabOb
}
}
+ // Delete child GameObjects (supports single string or array of paths/names)
+ JToken deleteChildToken = @params["deleteChild"] ?? @params["delete_child"];
+ if (deleteChildToken != null)
+ {
+ var deleteResult = RemoveChildren(deleteChildToken, targetGo, prefabRoot);
+ if (deleteResult.error != null)
+ {
+ return (false, deleteResult.error);
+ }
+ if (deleteResult.removedCount > 0)
+ {
+ modified = true;
+ }
+ }
+
// Set properties on existing components
JObject componentProperties = @params["componentProperties"] as JObject ?? @params["component_properties"] as JObject;
if (componentProperties != null && componentProperties.Count > 0)
@@ -1132,6 +1147,47 @@ private static (bool created, ErrorResponse error) CreateSingleChildInPrefab(JTo
return (true, null);
}
+ ///
+ /// Removes child GameObjects from a prefab.
+ ///
+ private static (int removedCount, ErrorResponse error) RemoveChildren(JToken deleteChildToken, GameObject targetGo, GameObject prefabRoot)
+ {
+ int removedCount = 0;
+
+ // Normalize to array
+ JArray childrenToDelete;
+ if (deleteChildToken is JArray arr)
+ {
+ childrenToDelete = arr;
+ }
+ else
+ {
+ childrenToDelete = new JArray { deleteChildToken };
+ }
+
+ foreach (var childToken in childrenToDelete)
+ {
+ string childPath = childToken.Type == JTokenType.String ? childToken.ToString() : childToken["name"]?.ToString();
+ if (string.IsNullOrEmpty(childPath))
+ {
+ return (removedCount, new ErrorResponse("'deleteChild'/'delete_child' entries must be a string or object with 'name' field."));
+ }
+
+ // Find the child to remove
+ Transform childToRemove = targetGo.transform.Find(childPath);
+ if (childToRemove == null)
+ {
+ return (removedCount, new ErrorResponse($"Child '{childPath}' not found under '{targetGo.name}'."));
+ }
+
+ UnityEngine.Object.DestroyImmediate(childToRemove.gameObject);
+ removedCount++;
+ McpLog.Info($"[ManagePrefabs] Removed child '{childPath}' under '{targetGo.name}' in prefab.");
+ }
+
+ return (removedCount, null);
+ }
+
#endregion
#region Hierarchy Builder
diff --git a/Server/src/cli/CLI_USAGE_GUIDE.md b/Server/src/cli/CLI_USAGE_GUIDE.md
index 4c6413eae..a276bd15a 100644
--- a/Server/src/cli/CLI_USAGE_GUIDE.md
+++ b/Server/src/cli/CLI_USAGE_GUIDE.md
@@ -560,6 +560,15 @@ unity-mcp prefab save
# Close prefab stage
unity-mcp prefab close
+
+# Modify prefab contents (headless)
+unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --target Weapon --position "0,1,2"
+unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --delete-child Child1 --delete-child "Turret/Barrel"
+unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --set-property "Rigidbody.mass=5"
+unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --add-component BoxCollider --remove-component SphereCollider
+unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --create-child '{"name":"Spawn","primitive_type":"Sphere"}'
+unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --name NewName --tag Player --layer UI
+unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --inactive
```
### UI Commands
diff --git a/Server/src/cli/commands/prefab.py b/Server/src/cli/commands/prefab.py
index 1081476d4..7278428de 100644
--- a/Server/src/cli/commands/prefab.py
+++ b/Server/src/cli/commands/prefab.py
@@ -1,5 +1,6 @@
"""Prefab CLI commands."""
+import json
import sys
import click
from typing import Optional, Any
@@ -246,3 +247,132 @@ def create(target: str, path: str, overwrite: bool, include_inactive: bool, unli
click.echo(format_output(result, config.format))
if result.get("success"):
print_success(f"Created prefab: {path}")
+
+
+def _parse_vector3(value: str) -> list[float]:
+ """Parse 'x,y,z' string to list of floats."""
+ parts = value.split(",")
+ if len(parts) != 3:
+ raise click.BadParameter("Must be 'x,y,z' format")
+ try:
+ return [float(p.strip()) for p in parts]
+ except ValueError as e:
+ raise click.BadParameter(f"All components must be numeric, got: '{value}'") from e
+
+
+def _parse_property(prop_str: str) -> tuple[str, str, Any]:
+ """Parse 'Component.prop=value' into (component, prop, value)."""
+ if "=" not in prop_str:
+ raise click.BadParameter("Must be 'Component.prop=value' format")
+ comp_prop, val_str = prop_str.split("=", 1)
+ if "." not in comp_prop:
+ raise click.BadParameter("Must be 'Component.prop=value' format")
+ component, prop = comp_prop.rsplit(".", 1)
+ if not component.strip() or not prop.strip():
+ raise click.BadParameter(f"Component and property must be non-empty in '{comp_prop}', expected 'Component.prop=value'")
+
+ val_str = val_str.strip()
+
+ # Parse booleans
+ if val_str.lower() == "true":
+ parsed_value: Any = True
+ elif val_str.lower() == "false":
+ parsed_value = False
+ # Parse numbers
+ elif "." in val_str:
+ try:
+ parsed_value = float(val_str)
+ except ValueError:
+ parsed_value = val_str
+ else:
+ try:
+ parsed_value = int(val_str)
+ except ValueError:
+ parsed_value = val_str
+
+ return component.strip(), prop.strip(), parsed_value
+
+
+@prefab.command("modify")
+@click.argument("path")
+@click.option("--target", "-t", help="Target object name/path within prefab (default: root)")
+@click.option("--position", "-p", help="New local position as 'x,y,z'")
+@click.option("--rotation", "-r", help="New local rotation as 'x,y,z'")
+@click.option("--scale", "-s", help="New local scale as 'x,y,z'")
+@click.option("--name", "-n", help="New name for target")
+@click.option("--tag", help="New tag")
+@click.option("--layer", help="New layer")
+@click.option("--active/--inactive", default=None, help="Set active state")
+@click.option("--parent", help="New parent object name/path")
+@click.option("--add-component", multiple=True, help="Component type to add (repeatable)")
+@click.option("--remove-component", multiple=True, help="Component type to remove (repeatable)")
+@click.option("--set-property", multiple=True, help="Property as 'Component.prop=value' (repeatable)")
+@click.option("--delete-child", multiple=True, help="Child name/path to remove (repeatable)")
+@click.option("--create-child", help="JSON object for child creation")
+@handle_unity_errors
+def modify(path: str, target: Optional[str], position: Optional[str], rotation: Optional[str],
+ scale: Optional[str], name: Optional[str], tag: Optional[str], layer: Optional[str],
+ active: Optional[bool], parent: Optional[str], add_component: tuple, remove_component: tuple,
+ set_property: tuple, delete_child: tuple, create_child: Optional[str]):
+ """Modify a prefab's contents (headless, no UI).
+
+ \b
+ Examples:
+ unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --delete-child Child1
+ unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --delete-child "Turret/Barrel" --delete-child Bullet
+ unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --target Weapon --position "0,1,2"
+ unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --set-property "Rigidbody.mass=5"
+ unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --create-child '{"name":"Spawn","primitive_type":"Sphere"}'
+ """
+ config = get_config()
+
+ params: dict[str, Any] = {
+ "action": "modify_contents",
+ "prefabPath": path,
+ }
+
+ if target:
+ params["target"] = target
+ if position:
+ params["position"] = _parse_vector3(position)
+ if rotation:
+ params["rotation"] = _parse_vector3(rotation)
+ if scale:
+ params["scale"] = _parse_vector3(scale)
+ if name:
+ params["name"] = name
+ if tag:
+ params["tag"] = tag
+ if layer:
+ params["layer"] = layer
+ if active is not None:
+ params["setActive"] = active
+ if parent:
+ params["parent"] = parent
+ if add_component:
+ params["componentsToAdd"] = list(add_component)
+ if remove_component:
+ params["componentsToRemove"] = list(remove_component)
+ if set_property:
+ component_properties: dict[str, dict[str, Any]] = {}
+ for prop in set_property:
+ comp, name_p, val = _parse_property(prop)
+ if comp not in component_properties:
+ component_properties[comp] = {}
+ component_properties[comp][name_p] = val
+ params["componentProperties"] = component_properties
+ if delete_child:
+ params["deleteChild"] = list(delete_child)
+ if create_child:
+ try:
+ parsed = json.loads(create_child)
+ except json.JSONDecodeError as e:
+ raise click.BadParameter(f"Invalid JSON for --create-child: {e}") from e
+ if not isinstance(parsed, dict):
+ raise click.BadParameter(f"--create-child must be a JSON object, got {type(parsed).__name__}")
+ params["createChild"] = parsed
+
+ result = run_command("manage_prefabs", params, config)
+ click.echo(format_output(result, config.format))
+ if result.get("success"):
+ print_success(f"Modified prefab: {path}")
diff --git a/Server/src/services/tools/manage_prefabs.py b/Server/src/services/tools/manage_prefabs.py
index bfa2f8cf1..e0a486d99 100644
--- a/Server/src/services/tools/manage_prefabs.py
+++ b/Server/src/services/tools/manage_prefabs.py
@@ -29,6 +29,9 @@
"(single object or array for batch creation in one save). "
"Example: create_child=[{\"name\": \"Child1\", \"primitive_type\": \"Sphere\", \"position\": [1,0,0]}, "
"{\"name\": \"Nested\", \"source_prefab_path\": \"Assets/Prefabs/Bullet.prefab\", \"position\": [0,2,0]}]. "
+ "Use delete_child parameter to remove child GameObjects from the prefab "
+ "(single name/path or array of paths for batch deletion. "
+ "Example: delete_child=[\"Child1\", \"Child2/Grandchild\"]). "
"Use component_properties with modify_contents to set serialized fields on existing components "
"(e.g. component_properties={\"Rigidbody\": {\"mass\": 5.0}, \"MyScript\": {\"health\": 100}}). "
"Supports object references via {\"guid\": \"...\"}, {\"path\": \"Assets/...\"}, or {\"instanceID\": 123}. "
@@ -67,6 +70,7 @@ async def manage_prefabs(
components_to_add: Annotated[list[str], "Component types to add in modify_contents."] | None = None,
components_to_remove: Annotated[list[str], "Component types to remove in modify_contents."] | None = None,
create_child: Annotated[dict[str, Any] | list[dict[str, Any]], "Create child GameObject(s) in the prefab. Single object or array of objects, each with: name (required), parent (optional, defaults to target), source_prefab_path (optional: asset path to instantiate as nested prefab, e.g. 'Assets/Prefabs/Bullet.prefab'), primitive_type (optional: Cube, Sphere, Capsule, Cylinder, Plane, Quad), position, rotation, scale, components_to_add, tag, layer, set_active. source_prefab_path and primitive_type are mutually exclusive."] | None = None,
+ delete_child: Annotated[str | list[str], "Child name(s) or path(s) to remove from the prefab. Supports single string or array for batch deletion (e.g. 'Child1' or ['Child1', 'Child1/Grandchild'])."] | None = None,
component_properties: Annotated[dict[str, dict[str, Any]], "Set properties on existing components in modify_contents. Keys are component type names, values are dicts of property name to value. Example: {\"Rigidbody\": {\"mass\": 5.0}, \"MyScript\": {\"health\": 100}}. Supports object references via {\"guid\": \"...\"}, {\"path\": \"Assets/...\"}, or {\"instanceID\": 123}. For Sprite sub-assets: {\"guid\": \"...\", \"spriteName\": \"\"}. Single-sprite textures auto-resolve."] | None = None,
) -> dict[str, Any]:
# Back-compat: map 'name' → 'target' for create_from_gameobject (Unity accepts both)
@@ -185,6 +189,9 @@ def normalize_child_params(child: Any, index: int | None = None) -> tuple[dict |
return {"success": False, "message": err}
params["createChild"] = child_params
+ if delete_child is not None:
+ params["deleteChild"] = delete_child
+
# Send command to Unity
response = await send_with_unity_instance(
async_send_command_with_retry, unity_instance, "manage_prefabs", params
diff --git a/Server/tests/test_cli.py b/Server/tests/test_cli.py
index 976b03cad..ccaebaa26 100644
--- a/Server/tests/test_cli.py
+++ b/Server/tests/test_cli.py
@@ -636,6 +636,214 @@ def test_prefab_create(self, runner, mock_unity_response):
])
assert result.exit_code == 0
+ def test_prefab_modify_delete_child(self, runner, mock_unity_response):
+ """Test prefab modify delete-child command."""
+ with patch("cli.commands.prefab.run_command", return_value=mock_unity_response) as mock_run:
+ result = runner.invoke(cli, [
+ "prefab", "modify", "Assets/Prefabs/Player.prefab",
+ "--delete-child", "Child1",
+ "--delete-child", "Turret/Barrel"
+ ])
+ assert result.exit_code == 0
+ # Verify the correct params were sent
+ call_args = mock_run.call_args[0]
+ assert call_args[0] == "manage_prefabs"
+ params = call_args[1]
+ assert params["action"] == "modify_contents"
+ assert params["deleteChild"] == ["Child1", "Turret/Barrel"]
+
+ def test_prefab_modify_transform(self, runner, mock_unity_response):
+ """Test prefab modify with transform options."""
+ with patch("cli.commands.prefab.run_command", return_value=mock_unity_response) as mock_run:
+ result = runner.invoke(cli, [
+ "prefab", "modify", "Assets/Prefabs/Player.prefab",
+ "--target", "Weapon",
+ "--position", "1,2,3",
+ "--rotation", "45,0,90",
+ "--scale", "2,2,2"
+ ])
+ assert result.exit_code == 0
+ call_args = mock_run.call_args[0]
+ params = call_args[1]
+ assert params["target"] == "Weapon"
+ assert params["position"] == [1.0, 2.0, 3.0]
+ assert params["rotation"] == [45.0, 0.0, 90.0]
+ assert params["scale"] == [2.0, 2.0, 2.0]
+
+ def test_prefab_modify_set_property(self, runner, mock_unity_response):
+ """Test prefab modify set-property command."""
+ with patch("cli.commands.prefab.run_command", return_value=mock_unity_response) as mock_run:
+ result = runner.invoke(cli, [
+ "prefab", "modify", "Assets/Prefabs/Player.prefab",
+ "--set-property", "Rigidbody.mass=5",
+ "--set-property", "Rigidbody.useGravity=false"
+ ])
+ assert result.exit_code == 0
+ call_args = mock_run.call_args[0]
+ params = call_args[1]
+ assert "componentProperties" in params
+ assert params["componentProperties"]["Rigidbody"]["mass"] == 5
+ assert params["componentProperties"]["Rigidbody"]["useGravity"] is False
+
+ def test_prefab_modify_set_property_float(self, runner, mock_unity_response):
+ """Test prefab modify set-property with float values."""
+ with patch("cli.commands.prefab.run_command", return_value=mock_unity_response) as mock_run:
+ result = runner.invoke(cli, [
+ "prefab", "modify", "Assets/Prefabs/Player.prefab",
+ "--set-property", "Rigidbody.mass=5.5"
+ ])
+ assert result.exit_code == 0
+ call_args = mock_run.call_args[0]
+ params = call_args[1]
+ assert params["componentProperties"]["Rigidbody"]["mass"] == 5.5
+
+ def test_prefab_modify_components(self, runner, mock_unity_response):
+ """Test prefab modify add/remove components."""
+ with patch("cli.commands.prefab.run_command", return_value=mock_unity_response) as mock_run:
+ result = runner.invoke(cli, [
+ "prefab", "modify", "Assets/Prefabs/Player.prefab",
+ "--add-component", "Rigidbody",
+ "--add-component", "BoxCollider",
+ "--remove-component", "SphereCollider"
+ ])
+ assert result.exit_code == 0
+ call_args = mock_run.call_args[0]
+ params = call_args[1]
+ assert params["componentsToAdd"] == ["Rigidbody", "BoxCollider"]
+ assert params["componentsToRemove"] == ["SphereCollider"]
+
+ def test_prefab_modify_create_child(self, runner, mock_unity_response):
+ """Test prefab modify create-child command."""
+ with patch("cli.commands.prefab.run_command", return_value=mock_unity_response) as mock_run:
+ result = runner.invoke(cli, [
+ "prefab", "modify", "Assets/Prefabs/Player.prefab",
+ "--create-child", '{"name":"Spawn","primitive_type":"Sphere"}'
+ ])
+ assert result.exit_code == 0
+ call_args = mock_run.call_args[0]
+ params = call_args[1]
+ assert params["createChild"]["name"] == "Spawn"
+ assert params["createChild"]["primitive_type"] == "Sphere"
+
+ def test_prefab_modify_invalid_create_child_json(self, runner, mock_unity_response):
+ """Test prefab modify with invalid JSON for create-child."""
+ result = runner.invoke(cli, [
+ "prefab", "modify", "Assets/Prefabs/Player.prefab",
+ "--create-child", "not valid json"
+ ])
+ assert result.exit_code != 0
+ assert "Invalid JSON" in result.output
+
+ def test_prefab_modify_create_child_non_object_json(self, runner, mock_unity_response):
+ """Test prefab modify rejects non-object JSON for create-child."""
+ result = runner.invoke(cli, [
+ "prefab", "modify", "Assets/Prefabs/Player.prefab",
+ "--create-child", '"just a string"'
+ ])
+ assert result.exit_code != 0
+ assert "must be a JSON object" in result.output
+
+ def test_prefab_modify_active_state(self, runner, mock_unity_response):
+ """Test prefab modify active/inactive flag."""
+ with patch("cli.commands.prefab.run_command", return_value=mock_unity_response) as mock_run:
+ result = runner.invoke(cli, [
+ "prefab", "modify", "Assets/Prefabs/Player.prefab",
+ "--inactive"
+ ])
+ assert result.exit_code == 0
+ call_args = mock_run.call_args[0]
+ params = call_args[1]
+ assert params["setActive"] is False
+
+ def test_prefab_modify_active_flag(self, runner, mock_unity_response):
+ """Test prefab modify --active flag."""
+ with patch("cli.commands.prefab.run_command", return_value=mock_unity_response) as mock_run:
+ result = runner.invoke(cli, [
+ "prefab", "modify", "Assets/Prefabs/Player.prefab",
+ "--active"
+ ])
+ assert result.exit_code == 0
+ call_args = mock_run.call_args[0]
+ params = call_args[1]
+ assert params["setActive"] is True
+
+ def test_prefab_modify_name_tag_layer_parent(self, runner, mock_unity_response):
+ """Test prefab modify with name, tag, layer, and parent options."""
+ with patch("cli.commands.prefab.run_command", return_value=mock_unity_response) as mock_run:
+ result = runner.invoke(cli, [
+ "prefab", "modify", "Assets/Prefabs/Player.prefab",
+ "--target", "Child1",
+ "--name", "RenamedChild",
+ "--tag", "Player",
+ "--layer", "UI",
+ "--parent", "NewParent"
+ ])
+ assert result.exit_code == 0
+ call_args = mock_run.call_args[0]
+ params = call_args[1]
+ assert params["target"] == "Child1"
+ assert params["name"] == "RenamedChild"
+ assert params["tag"] == "Player"
+ assert params["layer"] == "UI"
+ assert params["parent"] == "NewParent"
+
+ def test_prefab_modify_invalid_vector_non_numeric(self, runner, mock_unity_response):
+ """Test prefab modify rejects non-numeric vector components."""
+ result = runner.invoke(cli, [
+ "prefab", "modify", "Assets/Prefabs/Player.prefab",
+ "--position", "1,foo,3"
+ ])
+ assert result.exit_code != 0
+
+ def test_prefab_modify_invalid_vector_wrong_count(self, runner, mock_unity_response):
+ """Test prefab modify rejects vectors with wrong component count."""
+ result = runner.invoke(cli, [
+ "prefab", "modify", "Assets/Prefabs/Player.prefab",
+ "--position", "1,2"
+ ])
+ assert result.exit_code != 0
+
+ def test_prefab_modify_set_property_string_value(self, runner, mock_unity_response):
+ """Test prefab modify set-property with string values."""
+ with patch("cli.commands.prefab.run_command", return_value=mock_unity_response) as mock_run:
+ result = runner.invoke(cli, [
+ "prefab", "modify", "Assets/Prefabs/Player.prefab",
+ "--set-property", "MyScript.label=hello world"
+ ])
+ assert result.exit_code == 0
+ call_args = mock_run.call_args[0]
+ params = call_args[1]
+ assert params["componentProperties"]["MyScript"]["label"] == "hello world"
+
+ def test_prefab_modify_set_property_empty_component(self, runner, mock_unity_response):
+ """Test prefab modify rejects empty component name in set-property."""
+ result = runner.invoke(cli, [
+ "prefab", "modify", "Assets/Prefabs/Player.prefab",
+ "--set-property", ".mass=5"
+ ])
+ assert result.exit_code != 0
+ assert "non-empty" in result.output
+
+ def test_prefab_modify_set_property_empty_prop(self, runner, mock_unity_response):
+ """Test prefab modify rejects empty property name in set-property."""
+ result = runner.invoke(cli, [
+ "prefab", "modify", "Assets/Prefabs/Player.prefab",
+ "--set-property", "Rigidbody.=5"
+ ])
+ assert result.exit_code != 0
+ assert "non-empty" in result.output
+
+ def test_prefab_modify_no_options_sends_minimal_params(self, runner, mock_unity_response):
+ """Test prefab modify with no options sends only action and prefabPath."""
+ with patch("cli.commands.prefab.run_command", return_value=mock_unity_response) as mock_run:
+ result = runner.invoke(cli, [
+ "prefab", "modify", "Assets/Prefabs/Player.prefab"
+ ])
+ assert result.exit_code == 0
+ call_args = mock_run.call_args[0]
+ params = call_args[1]
+ assert params == {"action": "modify_contents", "prefabPath": "Assets/Prefabs/Player.prefab"}
+
# =============================================================================
# Material Command Tests
diff --git a/Server/tests/test_manage_prefabs.py b/Server/tests/test_manage_prefabs.py
index be441581c..ace42957d 100644
--- a/Server/tests/test_manage_prefabs.py
+++ b/Server/tests/test_manage_prefabs.py
@@ -1,8 +1,45 @@
-"""Tests for manage_prefabs tool - component_properties parameter."""
+"""Tests for manage_prefabs tool."""
+import asyncio
import inspect
+from types import SimpleNamespace
+from unittest.mock import AsyncMock
+
+import pytest
from services.tools.manage_prefabs import manage_prefabs
+from services.registry import get_registered_tools
+
+
+# ── Fixture ──────────────────────────────────────────────────────────
+
+
+@pytest.fixture
+def mock_unity(monkeypatch):
+ captured: dict[str, object] = {}
+
+ async def fake_send(send_fn, unity_instance, tool_name, params):
+ captured["unity_instance"] = unity_instance
+ captured["tool_name"] = tool_name
+ captured["params"] = params
+ return {"success": True, "message": "ok"}
+
+ monkeypatch.setattr(
+ "services.tools.manage_prefabs.get_unity_instance_from_context",
+ AsyncMock(return_value="unity-instance-1"),
+ )
+ monkeypatch.setattr(
+ "services.tools.manage_prefabs.send_with_unity_instance",
+ fake_send,
+ )
+ monkeypatch.setattr(
+ "services.tools.manage_prefabs.preflight",
+ AsyncMock(return_value=None),
+ )
+ return captured
+
+
+# ── component_properties ─────────────────────────────────────────────
class TestManagePrefabsComponentProperties:
@@ -21,13 +58,10 @@ def test_component_properties_parameter_is_optional(self):
def test_tool_description_mentions_component_properties(self):
"""The tool description should mention component_properties."""
- from services.registry import get_registered_tools
- tools = get_registered_tools()
prefab_tool = next(
- (t for t in tools if t["name"] == "manage_prefabs"), None
+ (t for t in get_registered_tools() if t["name"] == "manage_prefabs"), None
)
assert prefab_tool is not None
- # Description is stored at top level or in kwargs depending on how the decorator stores it
desc = prefab_tool.get("description") or prefab_tool.get("kwargs", {}).get("description", "")
assert "component_properties" in desc
@@ -36,3 +70,68 @@ def test_required_params_include_modify_contents(self):
from services.tools.manage_prefabs import REQUIRED_PARAMS
assert "modify_contents" in REQUIRED_PARAMS
assert "prefab_path" in REQUIRED_PARAMS["modify_contents"]
+
+
+# ── delete_child ─────────────────────────────────────────────────────
+
+
+class TestManagePrefabsDeleteChild:
+ """Tests for the delete_child parameter on manage_prefabs."""
+
+ def test_delete_child_parameter_exists(self):
+ """The manage_prefabs tool should have a delete_child parameter."""
+ sig = inspect.signature(manage_prefabs)
+ assert "delete_child" in sig.parameters
+
+ def test_delete_child_parameter_is_optional(self):
+ """delete_child should default to None."""
+ sig = inspect.signature(manage_prefabs)
+ param = sig.parameters["delete_child"]
+ assert param.default is None
+
+ def test_tool_description_mentions_delete_child(self):
+ """The tool description should mention delete_child."""
+ prefab_tool = next(
+ (t for t in get_registered_tools() if t["name"] == "manage_prefabs"), None
+ )
+ assert prefab_tool is not None
+ desc = prefab_tool.get("description") or prefab_tool.get("kwargs", {}).get("description", "")
+ assert "delete_child" in desc
+
+ def test_delete_child_string_forwards_to_unity(self, mock_unity):
+ """A single string delete_child should be forwarded as-is."""
+ result = asyncio.run(
+ manage_prefabs(
+ SimpleNamespace(),
+ action="modify_contents",
+ prefab_path="Assets/Prefabs/Test.prefab",
+ delete_child="Child1",
+ )
+ )
+ assert result["success"] is True
+ assert mock_unity["tool_name"] == "manage_prefabs"
+ assert mock_unity["params"]["deleteChild"] == "Child1"
+
+ def test_delete_child_list_forwards_to_unity(self, mock_unity):
+ """A list of delete_child paths should be forwarded as-is."""
+ result = asyncio.run(
+ manage_prefabs(
+ SimpleNamespace(),
+ action="modify_contents",
+ prefab_path="Assets/Prefabs/Test.prefab",
+ delete_child=["Child1", "Child2/Grandchild"],
+ )
+ )
+ assert result["success"] is True
+ assert mock_unity["params"]["deleteChild"] == ["Child1", "Child2/Grandchild"]
+
+ def test_delete_child_none_omitted_from_params(self, mock_unity):
+ """When delete_child is None, deleteChild should not appear in params."""
+ asyncio.run(
+ manage_prefabs(
+ SimpleNamespace(),
+ action="modify_contents",
+ prefab_path="Assets/Prefabs/Test.prefab",
+ )
+ )
+ assert "deleteChild" not in mock_unity["params"]
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManagePrefabsCrudTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManagePrefabsCrudTests.cs
index fb8ab86ef..b9f5ddcee 100644
--- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManagePrefabsCrudTests.cs
+++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManagePrefabsCrudTests.cs
@@ -722,6 +722,115 @@ public void ModifyContents_CreateChild_ReturnsErrorForInvalidInput()
#endregion
+ #region Delete Child Tests
+
+ [Test]
+ public void ModifyContents_DeleteChild_DeletesSingleChild()
+ {
+ string prefabPath = CreateNestedTestPrefab("DeleteSingleChild");
+
+ try
+ {
+ var result = ToJObject(ManagePrefabs.HandleCommand(new JObject
+ {
+ ["action"] = "modify_contents",
+ ["prefabPath"] = prefabPath,
+ ["deleteChild"] = "Child1"
+ }));
+
+ Assert.IsTrue(result.Value("success"), $"Expected success but got: {result}");
+
+ GameObject reloaded = AssetDatabase.LoadAssetAtPath(prefabPath);
+ Assert.IsNull(reloaded.transform.Find("Child1"), "Child1 should be deleted");
+ Assert.IsNotNull(reloaded.transform.Find("Child2"), "Child2 should still exist");
+ }
+ finally
+ {
+ SafeDeleteAsset(prefabPath);
+ }
+ }
+
+ [Test]
+ public void ModifyContents_DeleteChild_DeletesNestedChild()
+ {
+ string prefabPath = CreateNestedTestPrefab("DeleteNestedChild");
+
+ try
+ {
+ var result = ToJObject(ManagePrefabs.HandleCommand(new JObject
+ {
+ ["action"] = "modify_contents",
+ ["prefabPath"] = prefabPath,
+ ["target"] = "Child1",
+ ["deleteChild"] = "Grandchild"
+ }));
+
+ Assert.IsTrue(result.Value("success"), $"Expected success but got: {result}");
+
+ GameObject reloaded = AssetDatabase.LoadAssetAtPath(prefabPath);
+ Assert.IsNull(reloaded.transform.Find("Child1/Grandchild"), "Grandchild should be deleted");
+ Assert.IsNotNull(reloaded.transform.Find("Child1"), "Child1 should still exist");
+ }
+ finally
+ {
+ SafeDeleteAsset(prefabPath);
+ }
+ }
+
+ [Test]
+ public void ModifyContents_DeleteChild_DeletesMultipleChildrenFromArray()
+ {
+ string prefabPath = CreateNestedTestPrefab("DeleteMultipleChildren");
+
+ try
+ {
+ var result = ToJObject(ManagePrefabs.HandleCommand(new JObject
+ {
+ ["action"] = "modify_contents",
+ ["prefabPath"] = prefabPath,
+ ["deleteChild"] = new JArray { "Child1", "Child2" }
+ }));
+
+ Assert.IsTrue(result.Value("success"), $"Expected success but got: {result}");
+
+ GameObject reloaded = AssetDatabase.LoadAssetAtPath(prefabPath);
+ Assert.IsNull(reloaded.transform.Find("Child1"), "Child1 should be deleted");
+ Assert.IsNull(reloaded.transform.Find("Child2"), "Child2 should be deleted");
+ // Only the root should remain
+ Assert.AreEqual(0, reloaded.transform.childCount, "Root should have no children");
+ }
+ finally
+ {
+ SafeDeleteAsset(prefabPath);
+ }
+ }
+
+ [Test]
+ public void ModifyContents_DeleteChild_ReturnsErrorForNonexistentChild()
+ {
+ string prefabPath = CreateNestedTestPrefab("DeleteNonexistentChild");
+
+ try
+ {
+ var result = ToJObject(ManagePrefabs.HandleCommand(new JObject
+ {
+ ["action"] = "modify_contents",
+ ["prefabPath"] = prefabPath,
+ ["deleteChild"] = "DeleteNonexistentChild" // This also tests whether it searches itself
+ }));
+
+ Assert.IsFalse(result.Value("success"));
+ Assert.IsTrue(result.Value("error").Contains("not found"),
+ $"Expected 'not found' error but got: {result.Value("error")}");
+ }
+ finally
+ {
+ SafeDeleteAsset(prefabPath);
+ }
+ }
+
+ #endregion
+
#region Component Properties Tests
[Test]
diff --git a/docs/guides/CLI_EXAMPLE.md b/docs/guides/CLI_EXAMPLE.md
index dbddaee81..6034b5147 100644
--- a/docs/guides/CLI_EXAMPLE.md
+++ b/docs/guides/CLI_EXAMPLE.md
@@ -120,6 +120,9 @@ unity-mcp prefab open "Assets/Prefabs/File.prefab"
unity-mcp prefab save
unity-mcp prefab close
unity-mcp prefab create "GameObject" --path "Assets/Prefabs"
+unity-mcp prefab modify "Assets/Prefabs/File.prefab" --delete-child Child1
+unity-mcp prefab modify "Assets/Prefabs/File.prefab" --target Weapon --position "0,1,2"
+unity-mcp prefab modify "Assets/Prefabs/File.prefab" --set-property "Rigidbody.mass=5"
```
**Material Operations**
diff --git a/docs/guides/CLI_USAGE.md b/docs/guides/CLI_USAGE.md
index 0c610503f..062dfb524 100644
--- a/docs/guides/CLI_USAGE.md
+++ b/docs/guides/CLI_USAGE.md
@@ -298,6 +298,13 @@ unity-mcp prefab close
# Create from GameObject
unity-mcp prefab create "Player" --path "Assets/Prefabs"
+
+# Modify prefab contents (headless, no UI)
+unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --target Weapon --position "0,1,2"
+unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --delete-child Child1 --delete-child "Turret/Barrel"
+unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --set-property "Rigidbody.mass=5"
+unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --add-component BoxCollider
+unity-mcp prefab modify "Assets/Prefabs/Player.prefab" --create-child '{"name":"Spawn","primitive_type":"Sphere"}'
```
### Asset Operations
@@ -487,7 +494,7 @@ unity-mcp raw manage_packages '{"action": "list_packages"}'
| `shader` | `create`, `read`, `update`, `delete` |
| `editor` | `play`, `pause`, `stop`, `refresh`, `console`, `menu`, `tool`, `add-tag`, `remove-tag`, `add-layer`, `remove-layer`, `tests`, `poll-test`, `custom-tool` |
| `asset` | `search`, `info`, `create`, `delete`, `duplicate`, `move`, `rename`, `import`, `mkdir` |
-| `prefab` | `open`, `close`, `save`, `create` |
+| `prefab` | `open`, `close`, `save`, `create`, `modify` |
| `material` | `info`, `create`, `set-color`, `set-property`, `assign`, `set-renderer-color` |
| `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` |
| `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` |
diff --git a/unity-mcp-skill/references/tools-reference.md b/unity-mcp-skill/references/tools-reference.md
index 5ac557510..9bf9cb9ce 100644
--- a/unity-mcp-skill/references/tools-reference.md
+++ b/unity-mcp-skill/references/tools-reference.md
@@ -524,6 +524,28 @@ manage_prefabs(
position=[0, 1, 0],
components_to_add=["AudioSource"]
)
+
+# Delete child GameObjects from prefab
+manage_prefabs(
+ action="modify_contents",
+ prefab_path="Assets/Prefabs/Player.prefab",
+ delete_child=["OldChild", "Turret/Barrel"] # single string or list
+)
+
+# Create child GameObject in prefab
+manage_prefabs(
+ action="modify_contents",
+ prefab_path="Assets/Prefabs/Player.prefab",
+ create_child={"name": "SpawnPoint", "primitive_type": "Sphere", "position": [0, 2, 0]}
+)
+
+# Set component properties on prefab contents
+manage_prefabs(
+ action="modify_contents",
+ prefab_path="Assets/Prefabs/Player.prefab",
+ target="ChildObject",
+ component_properties={"Rigidbody": {"mass": 5.0}, "MyScript": {"health": 100}}
+)
```
---