diff --git a/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs b/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs
index 9b4df7be8..6df9d47bf 100644
--- a/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs
+++ b/MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs
@@ -723,9 +723,188 @@ private static (bool modified, ErrorResponse error) ApplyModificationsToPrefabOb
}
}
+ // Create child GameObjects (supports single object or array)
+ JToken createChildToken = @params["createChild"] ?? @params["create_child"];
+ if (createChildToken != null)
+ {
+ // Handle array of children
+ if (createChildToken is JArray childArray)
+ {
+ foreach (var childToken in childArray)
+ {
+ var childResult = CreateSingleChildInPrefab(childToken, targetGo, prefabRoot);
+ if (childResult.error != null)
+ {
+ return (false, childResult.error);
+ }
+ if (childResult.created)
+ {
+ modified = true;
+ }
+ }
+ }
+ else
+ {
+ // Handle single child object
+ var childResult = CreateSingleChildInPrefab(createChildToken, targetGo, prefabRoot);
+ if (childResult.error != null)
+ {
+ return (false, childResult.error);
+ }
+ if (childResult.created)
+ {
+ modified = true;
+ }
+ }
+ }
+
return (modified, null);
}
+ ///
+ /// Creates a single child GameObject within the prefab contents.
+ ///
+ private static (bool created, ErrorResponse error) CreateSingleChildInPrefab(JToken createChildToken, GameObject defaultParent, GameObject prefabRoot)
+ {
+ JObject childParams;
+ if (createChildToken is JObject obj)
+ {
+ childParams = obj;
+ }
+ else
+ {
+ return (false, new ErrorResponse("'create_child' must be an object with child properties."));
+ }
+
+ // Required: name
+ string childName = childParams["name"]?.ToString();
+ if (string.IsNullOrEmpty(childName))
+ {
+ return (false, new ErrorResponse("'create_child.name' is required."));
+ }
+
+ // Optional: parent (defaults to the target object)
+ string parentName = childParams["parent"]?.ToString();
+ Transform parentTransform = defaultParent.transform;
+ if (!string.IsNullOrEmpty(parentName))
+ {
+ GameObject parentGo = FindInPrefabContents(prefabRoot, parentName);
+ if (parentGo == null)
+ {
+ return (false, new ErrorResponse($"Parent '{parentName}' not found in prefab for create_child."));
+ }
+ parentTransform = parentGo.transform;
+ }
+
+ // Create the GameObject
+ GameObject newChild;
+ string primitiveType = childParams["primitiveType"]?.ToString() ?? childParams["primitive_type"]?.ToString();
+ if (!string.IsNullOrEmpty(primitiveType))
+ {
+ try
+ {
+ PrimitiveType type = (PrimitiveType)Enum.Parse(typeof(PrimitiveType), primitiveType, true);
+ newChild = GameObject.CreatePrimitive(type);
+ newChild.name = childName;
+ }
+ catch (ArgumentException)
+ {
+ return (false, new ErrorResponse($"Invalid primitive type: '{primitiveType}'. Valid types: {string.Join(", ", Enum.GetNames(typeof(PrimitiveType)))}"));
+ }
+ }
+ else
+ {
+ newChild = new GameObject(childName);
+ }
+
+ // Set parent
+ newChild.transform.SetParent(parentTransform, false);
+
+ // Apply transform properties
+ Vector3? position = VectorParsing.ParseVector3(childParams["position"]);
+ Vector3? rotation = VectorParsing.ParseVector3(childParams["rotation"]);
+ Vector3? scale = VectorParsing.ParseVector3(childParams["scale"]);
+
+ if (position.HasValue)
+ {
+ newChild.transform.localPosition = position.Value;
+ }
+ if (rotation.HasValue)
+ {
+ newChild.transform.localEulerAngles = rotation.Value;
+ }
+ if (scale.HasValue)
+ {
+ newChild.transform.localScale = scale.Value;
+ }
+
+ // Add components
+ JArray componentsToAdd = childParams["componentsToAdd"] as JArray ?? childParams["components_to_add"] as JArray;
+ if (componentsToAdd != null)
+ {
+ for (int i = 0; i < componentsToAdd.Count; i++)
+ {
+ var compToken = componentsToAdd[i];
+ string typeName = compToken.Type == JTokenType.String
+ ? compToken.ToString()
+ : (compToken as JObject)?["typeName"]?.ToString();
+
+ if (string.IsNullOrEmpty(typeName))
+ {
+ // Clean up partially created child
+ UnityEngine.Object.DestroyImmediate(newChild);
+ return (false, new ErrorResponse($"create_child.components_to_add[{i}] must be a string or object with 'typeName' field, got {compToken.Type}"));
+ }
+
+ if (!ComponentResolver.TryResolve(typeName, out Type componentType, out string error))
+ {
+ // Clean up partially created child
+ UnityEngine.Object.DestroyImmediate(newChild);
+ return (false, new ErrorResponse($"Component type '{typeName}' not found for create_child: {error}"));
+ }
+ newChild.AddComponent(componentType);
+ }
+ }
+
+ // Set tag if specified
+ string tag = childParams["tag"]?.ToString();
+ if (!string.IsNullOrEmpty(tag))
+ {
+ try
+ {
+ newChild.tag = tag;
+ }
+ catch (Exception ex)
+ {
+ UnityEngine.Object.DestroyImmediate(newChild);
+ return (false, new ErrorResponse($"Failed to set tag '{tag}' on child '{childName}': {ex.Message}"));
+ }
+ }
+
+ // Set layer if specified
+ string layerName = childParams["layer"]?.ToString();
+ if (!string.IsNullOrEmpty(layerName))
+ {
+ int layerId = LayerMask.NameToLayer(layerName);
+ if (layerId == -1)
+ {
+ UnityEngine.Object.DestroyImmediate(newChild);
+ return (false, new ErrorResponse($"Invalid layer '{layerName}' for child '{childName}'. Use a valid layer name."));
+ }
+ newChild.layer = layerId;
+ }
+
+ // Set active state
+ bool? setActive = childParams["setActive"]?.ToObject() ?? childParams["set_active"]?.ToObject();
+ if (setActive.HasValue)
+ {
+ newChild.SetActive(setActive.Value);
+ }
+
+ McpLog.Info($"[ManagePrefabs] Created child '{childName}' under '{parentTransform.name}' in prefab.");
+ return (true, null);
+ }
+
#endregion
#region Hierarchy Builder
diff --git a/Server/src/services/tools/manage_prefabs.py b/Server/src/services/tools/manage_prefabs.py
index 90dd127d7..15df5f803 100644
--- a/Server/src/services/tools/manage_prefabs.py
+++ b/Server/src/services/tools/manage_prefabs.py
@@ -25,6 +25,10 @@
"Manages Unity Prefab assets via headless operations (no UI, no prefab stages). "
"Actions: get_info, get_hierarchy, create_from_gameobject, modify_contents. "
"Use modify_contents for headless prefab editing - ideal for automated workflows. "
+ "Use create_child parameter with modify_contents to add child GameObjects to a prefab "
+ "(single object or array for batch creation in one save). "
+ "Example: create_child=[{\"name\": \"Child1\", \"primitive_type\": \"Sphere\", \"position\": [1,0,0]}, "
+ "{\"name\": \"Child2\", \"primitive_type\": \"Cube\", \"parent\": \"Child1\"}]. "
"Use manage_asset action=search filterType=Prefab to list prefabs."
),
annotations=ToolAnnotations(
@@ -59,6 +63,7 @@ async def manage_prefabs(
parent: Annotated[str, "New parent object name/path within prefab for modify_contents."] | None = None,
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), primitive_type (optional: Cube, Sphere, Capsule, Cylinder, Plane, Quad), position, rotation, scale, components_to_add, tag, layer, set_active."] | None = None,
) -> dict[str, Any]:
# Back-compat: map 'name' → 'target' for create_from_gameobject (Unity accepts both)
if action == "create_from_gameobject" and target is None and name is not None:
@@ -143,6 +148,36 @@ async def manage_prefabs(
params["componentsToAdd"] = components_to_add
if components_to_remove is not None:
params["componentsToRemove"] = components_to_remove
+ if create_child is not None:
+ # Normalize vector fields within create_child (handles single object or array)
+ def normalize_child_params(child: Any, index: int | None = None) -> tuple[dict | None, str | None]:
+ prefix = f"create_child[{index}]" if index is not None else "create_child"
+ if not isinstance(child, dict):
+ return None, f"{prefix} must be a dict with child properties (name, primitive_type, position, etc.), got {type(child).__name__}"
+ child_params = dict(child)
+ for vec_field in ("position", "rotation", "scale"):
+ if vec_field in child_params and child_params[vec_field] is not None:
+ vec_val, vec_err = normalize_vector3(child_params[vec_field], f"{prefix}.{vec_field}")
+ if vec_err:
+ return None, vec_err
+ child_params[vec_field] = vec_val
+ return child_params, None
+
+ if isinstance(create_child, list):
+ # Array of children
+ normalized_children = []
+ for i, child in enumerate(create_child):
+ child_params, err = normalize_child_params(child, i)
+ if err:
+ return {"success": False, "message": err}
+ normalized_children.append(child_params)
+ params["createChild"] = normalized_children
+ else:
+ # Single child object
+ child_params, err = normalize_child_params(create_child)
+ if err:
+ return {"success": False, "message": err}
+ params["createChild"] = child_params
# Send command to Unity
response = await send_with_unity_instance(
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManagePrefabsCrudTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManagePrefabsCrudTests.cs
index da6c5063d..b4949224d 100644
--- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManagePrefabsCrudTests.cs
+++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/ManagePrefabsCrudTests.cs
@@ -532,6 +532,194 @@ public void ModifyContents_PreventsHierarchyLoops()
}
}
+ [Test]
+ public void ModifyContents_CreateChild_AddsSingleChildWithPrimitive()
+ {
+ string prefabPath = CreateTestPrefab("CreateChildTest");
+
+ try
+ {
+ var result = ToJObject(ManagePrefabs.HandleCommand(new JObject
+ {
+ ["action"] = "modify_contents",
+ ["prefabPath"] = prefabPath,
+ ["createChild"] = new JObject
+ {
+ ["name"] = "NewSphere",
+ ["primitive_type"] = "Sphere",
+ ["position"] = new JArray(1f, 2f, 3f),
+ ["scale"] = new JArray(0.5f, 0.5f, 0.5f)
+ }
+ }));
+
+ Assert.IsTrue(result.Value("success"));
+
+ GameObject reloaded = AssetDatabase.LoadAssetAtPath(prefabPath);
+ Transform child = reloaded.transform.Find("NewSphere");
+ Assert.IsNotNull(child, "Child should exist");
+ Assert.AreEqual(new Vector3(1f, 2f, 3f), child.localPosition);
+ Assert.AreEqual(new Vector3(0.5f, 0.5f, 0.5f), child.localScale);
+ Assert.IsNotNull(child.GetComponent(), "Sphere primitive should have SphereCollider");
+ }
+ finally
+ {
+ SafeDeleteAsset(prefabPath);
+ }
+ }
+
+ [Test]
+ public void ModifyContents_CreateChild_AddsEmptyGameObject()
+ {
+ string prefabPath = CreateTestPrefab("EmptyChildTest");
+
+ try
+ {
+ var result = ToJObject(ManagePrefabs.HandleCommand(new JObject
+ {
+ ["action"] = "modify_contents",
+ ["prefabPath"] = prefabPath,
+ ["createChild"] = new JObject
+ {
+ ["name"] = "EmptyChild",
+ ["position"] = new JArray(0f, 5f, 0f)
+ }
+ }));
+
+ Assert.IsTrue(result.Value("success"));
+
+ GameObject reloaded = AssetDatabase.LoadAssetAtPath(prefabPath);
+ Transform child = reloaded.transform.Find("EmptyChild");
+ Assert.IsNotNull(child, "Empty child should exist");
+ Assert.AreEqual(new Vector3(0f, 5f, 0f), child.localPosition);
+ // Empty GO should only have Transform
+ Assert.AreEqual(1, child.GetComponents().Length, "Empty child should only have Transform");
+ }
+ finally
+ {
+ SafeDeleteAsset(prefabPath);
+ }
+ }
+
+ [Test]
+ public void ModifyContents_CreateChild_AddsMultipleChildrenFromArray()
+ {
+ string prefabPath = CreateTestPrefab("MultiChildTest");
+
+ try
+ {
+ var result = ToJObject(ManagePrefabs.HandleCommand(new JObject
+ {
+ ["action"] = "modify_contents",
+ ["prefabPath"] = prefabPath,
+ ["createChild"] = new JArray
+ {
+ new JObject { ["name"] = "Child1", ["primitive_type"] = "Cube", ["position"] = new JArray(1f, 0f, 0f) },
+ new JObject { ["name"] = "Child2", ["primitive_type"] = "Sphere", ["position"] = new JArray(-1f, 0f, 0f) },
+ new JObject { ["name"] = "Child3", ["position"] = new JArray(0f, 1f, 0f) }
+ }
+ }));
+
+ Assert.IsTrue(result.Value("success"));
+
+ GameObject reloaded = AssetDatabase.LoadAssetAtPath(prefabPath);
+ Assert.IsNotNull(reloaded.transform.Find("Child1"), "Child1 should exist");
+ Assert.IsNotNull(reloaded.transform.Find("Child2"), "Child2 should exist");
+ Assert.IsNotNull(reloaded.transform.Find("Child3"), "Child3 should exist");
+ Assert.IsNotNull(reloaded.transform.Find("Child1").GetComponent(), "Child1 should be Cube");
+ Assert.IsNotNull(reloaded.transform.Find("Child2").GetComponent(), "Child2 should be Sphere");
+ }
+ finally
+ {
+ SafeDeleteAsset(prefabPath);
+ }
+ }
+
+ [Test]
+ public void ModifyContents_CreateChild_SupportsNestedParenting()
+ {
+ string prefabPath = CreateNestedTestPrefab("NestedCreateChildTest");
+
+ try
+ {
+ var result = ToJObject(ManagePrefabs.HandleCommand(new JObject
+ {
+ ["action"] = "modify_contents",
+ ["prefabPath"] = prefabPath,
+ ["createChild"] = new JObject
+ {
+ ["name"] = "NewGrandchild",
+ ["parent"] = "Child1",
+ ["primitive_type"] = "Capsule"
+ }
+ }));
+
+ Assert.IsTrue(result.Value("success"));
+
+ GameObject reloaded = AssetDatabase.LoadAssetAtPath(prefabPath);
+ Transform newChild = reloaded.transform.Find("Child1/NewGrandchild");
+ Assert.IsNotNull(newChild, "NewGrandchild should be under Child1");
+ Assert.IsNotNull(newChild.GetComponent(), "Should be Capsule primitive");
+ }
+ finally
+ {
+ SafeDeleteAsset(prefabPath);
+ }
+ }
+
+ [Test]
+ public void ModifyContents_CreateChild_ReturnsErrorForInvalidInput()
+ {
+ string prefabPath = CreateTestPrefab("InvalidChildTest");
+
+ try
+ {
+ // Missing required 'name' field
+ var missingName = ToJObject(ManagePrefabs.HandleCommand(new JObject
+ {
+ ["action"] = "modify_contents",
+ ["prefabPath"] = prefabPath,
+ ["createChild"] = new JObject
+ {
+ ["primitive_type"] = "Cube"
+ }
+ }));
+ Assert.IsFalse(missingName.Value("success"));
+ Assert.IsTrue(missingName.Value("error").Contains("name"));
+
+ // Invalid parent
+ var invalidParent = ToJObject(ManagePrefabs.HandleCommand(new JObject
+ {
+ ["action"] = "modify_contents",
+ ["prefabPath"] = prefabPath,
+ ["createChild"] = new JObject
+ {
+ ["name"] = "TestChild",
+ ["parent"] = "NonexistentParent"
+ }
+ }));
+ Assert.IsFalse(invalidParent.Value("success"));
+ Assert.IsTrue(invalidParent.Value("error").Contains("not found"));
+
+ // Invalid primitive type
+ var invalidPrimitive = ToJObject(ManagePrefabs.HandleCommand(new JObject
+ {
+ ["action"] = "modify_contents",
+ ["prefabPath"] = prefabPath,
+ ["createChild"] = new JObject
+ {
+ ["name"] = "TestChild",
+ ["primitive_type"] = "InvalidType"
+ }
+ }));
+ Assert.IsFalse(invalidPrimitive.Value("success"));
+ Assert.IsTrue(invalidPrimitive.Value("error").Contains("Invalid primitive type"));
+ }
+ finally
+ {
+ SafeDeleteAsset(prefabPath);
+ }
+ }
+
#endregion
#region Error Handling