diff --git a/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs b/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs
index b24eee044..664c976c2 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("'delete_child' 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/commands/prefab.py b/Server/src/cli/commands/prefab.py
index 1081476d4..c0226db2e 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,124 @@ 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")
+ return [float(p.strip()) for p in parts]
+
+
+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)
+
+ 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:
+ params["createChild"] = json.loads(create_child)
+ except json.JSONDecodeError as e:
+ raise click.BadParameter(f"Invalid JSON for --create-child: {e}")
+
+ 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..e15f5bfd0 100644
--- a/Server/tests/test_cli.py
+++ b/Server/tests/test_cli.py
@@ -636,6 +636,115 @@ 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
+
+ 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
+
# =============================================================================
# Material Command Tests
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]