Skip to content

Commit 62c015d

Browse files
dsarnoclaude
andauthored
Add create_child parameter to manage_prefabs modify_contents (#646)
* Add create_child parameter to manage_prefabs modify_contents Enables adding child GameObjects to existing prefabs via headless editing. Supports single object or array for batch creation in one save operation. Features: - Create children with primitive types (Cube, Sphere, etc.) - Set position, rotation, scale on new children - Add components to children - Specify parent within prefab hierarchy for nested children - Set tag, layer, and active state Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Address code review feedback for create_child validation - Fix type hint to `tuple[dict | None, str | None]` to match actual returns - Add explicit dict validation with clear error message including actual type - Error on invalid component entries instead of silently ignoring them - Return ErrorResponse for invalid tag/layer instead of just logging warnings Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add unit tests for create_child prefab functionality Tests cover: - Single child with primitive type - Empty GameObject (no primitive_type) - Multiple children from array (batch creation) - Nested parenting within prefab - Error handling for invalid inputs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 3d3d45e commit 62c015d

3 files changed

Lines changed: 402 additions & 0 deletions

File tree

‎MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs‎

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,9 +723,188 @@ private static (bool modified, ErrorResponse error) ApplyModificationsToPrefabOb
723723
}
724724
}
725725

726+
// Create child GameObjects (supports single object or array)
727+
JToken createChildToken = @params["createChild"] ?? @params["create_child"];
728+
if (createChildToken != null)
729+
{
730+
// Handle array of children
731+
if (createChildToken is JArray childArray)
732+
{
733+
foreach (var childToken in childArray)
734+
{
735+
var childResult = CreateSingleChildInPrefab(childToken, targetGo, prefabRoot);
736+
if (childResult.error != null)
737+
{
738+
return (false, childResult.error);
739+
}
740+
if (childResult.created)
741+
{
742+
modified = true;
743+
}
744+
}
745+
}
746+
else
747+
{
748+
// Handle single child object
749+
var childResult = CreateSingleChildInPrefab(createChildToken, targetGo, prefabRoot);
750+
if (childResult.error != null)
751+
{
752+
return (false, childResult.error);
753+
}
754+
if (childResult.created)
755+
{
756+
modified = true;
757+
}
758+
}
759+
}
760+
726761
return (modified, null);
727762
}
728763

764+
/// <summary>
765+
/// Creates a single child GameObject within the prefab contents.
766+
/// </summary>
767+
private static (bool created, ErrorResponse error) CreateSingleChildInPrefab(JToken createChildToken, GameObject defaultParent, GameObject prefabRoot)
768+
{
769+
JObject childParams;
770+
if (createChildToken is JObject obj)
771+
{
772+
childParams = obj;
773+
}
774+
else
775+
{
776+
return (false, new ErrorResponse("'create_child' must be an object with child properties."));
777+
}
778+
779+
// Required: name
780+
string childName = childParams["name"]?.ToString();
781+
if (string.IsNullOrEmpty(childName))
782+
{
783+
return (false, new ErrorResponse("'create_child.name' is required."));
784+
}
785+
786+
// Optional: parent (defaults to the target object)
787+
string parentName = childParams["parent"]?.ToString();
788+
Transform parentTransform = defaultParent.transform;
789+
if (!string.IsNullOrEmpty(parentName))
790+
{
791+
GameObject parentGo = FindInPrefabContents(prefabRoot, parentName);
792+
if (parentGo == null)
793+
{
794+
return (false, new ErrorResponse($"Parent '{parentName}' not found in prefab for create_child."));
795+
}
796+
parentTransform = parentGo.transform;
797+
}
798+
799+
// Create the GameObject
800+
GameObject newChild;
801+
string primitiveType = childParams["primitiveType"]?.ToString() ?? childParams["primitive_type"]?.ToString();
802+
if (!string.IsNullOrEmpty(primitiveType))
803+
{
804+
try
805+
{
806+
PrimitiveType type = (PrimitiveType)Enum.Parse(typeof(PrimitiveType), primitiveType, true);
807+
newChild = GameObject.CreatePrimitive(type);
808+
newChild.name = childName;
809+
}
810+
catch (ArgumentException)
811+
{
812+
return (false, new ErrorResponse($"Invalid primitive type: '{primitiveType}'. Valid types: {string.Join(", ", Enum.GetNames(typeof(PrimitiveType)))}"));
813+
}
814+
}
815+
else
816+
{
817+
newChild = new GameObject(childName);
818+
}
819+
820+
// Set parent
821+
newChild.transform.SetParent(parentTransform, false);
822+
823+
// Apply transform properties
824+
Vector3? position = VectorParsing.ParseVector3(childParams["position"]);
825+
Vector3? rotation = VectorParsing.ParseVector3(childParams["rotation"]);
826+
Vector3? scale = VectorParsing.ParseVector3(childParams["scale"]);
827+
828+
if (position.HasValue)
829+
{
830+
newChild.transform.localPosition = position.Value;
831+
}
832+
if (rotation.HasValue)
833+
{
834+
newChild.transform.localEulerAngles = rotation.Value;
835+
}
836+
if (scale.HasValue)
837+
{
838+
newChild.transform.localScale = scale.Value;
839+
}
840+
841+
// Add components
842+
JArray componentsToAdd = childParams["componentsToAdd"] as JArray ?? childParams["components_to_add"] as JArray;
843+
if (componentsToAdd != null)
844+
{
845+
for (int i = 0; i < componentsToAdd.Count; i++)
846+
{
847+
var compToken = componentsToAdd[i];
848+
string typeName = compToken.Type == JTokenType.String
849+
? compToken.ToString()
850+
: (compToken as JObject)?["typeName"]?.ToString();
851+
852+
if (string.IsNullOrEmpty(typeName))
853+
{
854+
// Clean up partially created child
855+
UnityEngine.Object.DestroyImmediate(newChild);
856+
return (false, new ErrorResponse($"create_child.components_to_add[{i}] must be a string or object with 'typeName' field, got {compToken.Type}"));
857+
}
858+
859+
if (!ComponentResolver.TryResolve(typeName, out Type componentType, out string error))
860+
{
861+
// Clean up partially created child
862+
UnityEngine.Object.DestroyImmediate(newChild);
863+
return (false, new ErrorResponse($"Component type '{typeName}' not found for create_child: {error}"));
864+
}
865+
newChild.AddComponent(componentType);
866+
}
867+
}
868+
869+
// Set tag if specified
870+
string tag = childParams["tag"]?.ToString();
871+
if (!string.IsNullOrEmpty(tag))
872+
{
873+
try
874+
{
875+
newChild.tag = tag;
876+
}
877+
catch (Exception ex)
878+
{
879+
UnityEngine.Object.DestroyImmediate(newChild);
880+
return (false, new ErrorResponse($"Failed to set tag '{tag}' on child '{childName}': {ex.Message}"));
881+
}
882+
}
883+
884+
// Set layer if specified
885+
string layerName = childParams["layer"]?.ToString();
886+
if (!string.IsNullOrEmpty(layerName))
887+
{
888+
int layerId = LayerMask.NameToLayer(layerName);
889+
if (layerId == -1)
890+
{
891+
UnityEngine.Object.DestroyImmediate(newChild);
892+
return (false, new ErrorResponse($"Invalid layer '{layerName}' for child '{childName}'. Use a valid layer name."));
893+
}
894+
newChild.layer = layerId;
895+
}
896+
897+
// Set active state
898+
bool? setActive = childParams["setActive"]?.ToObject<bool?>() ?? childParams["set_active"]?.ToObject<bool?>();
899+
if (setActive.HasValue)
900+
{
901+
newChild.SetActive(setActive.Value);
902+
}
903+
904+
McpLog.Info($"[ManagePrefabs] Created child '{childName}' under '{parentTransform.name}' in prefab.");
905+
return (true, null);
906+
}
907+
729908
#endregion
730909

731910
#region Hierarchy Builder

‎Server/src/services/tools/manage_prefabs.py‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@
2525
"Manages Unity Prefab assets via headless operations (no UI, no prefab stages). "
2626
"Actions: get_info, get_hierarchy, create_from_gameobject, modify_contents. "
2727
"Use modify_contents for headless prefab editing - ideal for automated workflows. "
28+
"Use create_child parameter with modify_contents to add child GameObjects to a prefab "
29+
"(single object or array for batch creation in one save). "
30+
"Example: create_child=[{\"name\": \"Child1\", \"primitive_type\": \"Sphere\", \"position\": [1,0,0]}, "
31+
"{\"name\": \"Child2\", \"primitive_type\": \"Cube\", \"parent\": \"Child1\"}]. "
2832
"Use manage_asset action=search filterType=Prefab to list prefabs."
2933
),
3034
annotations=ToolAnnotations(
@@ -59,6 +63,7 @@ async def manage_prefabs(
5963
parent: Annotated[str, "New parent object name/path within prefab for modify_contents."] | None = None,
6064
components_to_add: Annotated[list[str], "Component types to add in modify_contents."] | None = None,
6165
components_to_remove: Annotated[list[str], "Component types to remove in modify_contents."] | None = None,
66+
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,
6267
) -> dict[str, Any]:
6368
# Back-compat: map 'name' → 'target' for create_from_gameobject (Unity accepts both)
6469
if action == "create_from_gameobject" and target is None and name is not None:
@@ -143,6 +148,36 @@ async def manage_prefabs(
143148
params["componentsToAdd"] = components_to_add
144149
if components_to_remove is not None:
145150
params["componentsToRemove"] = components_to_remove
151+
if create_child is not None:
152+
# Normalize vector fields within create_child (handles single object or array)
153+
def normalize_child_params(child: Any, index: int | None = None) -> tuple[dict | None, str | None]:
154+
prefix = f"create_child[{index}]" if index is not None else "create_child"
155+
if not isinstance(child, dict):
156+
return None, f"{prefix} must be a dict with child properties (name, primitive_type, position, etc.), got {type(child).__name__}"
157+
child_params = dict(child)
158+
for vec_field in ("position", "rotation", "scale"):
159+
if vec_field in child_params and child_params[vec_field] is not None:
160+
vec_val, vec_err = normalize_vector3(child_params[vec_field], f"{prefix}.{vec_field}")
161+
if vec_err:
162+
return None, vec_err
163+
child_params[vec_field] = vec_val
164+
return child_params, None
165+
166+
if isinstance(create_child, list):
167+
# Array of children
168+
normalized_children = []
169+
for i, child in enumerate(create_child):
170+
child_params, err = normalize_child_params(child, i)
171+
if err:
172+
return {"success": False, "message": err}
173+
normalized_children.append(child_params)
174+
params["createChild"] = normalized_children
175+
else:
176+
# Single child object
177+
child_params, err = normalize_child_params(create_child)
178+
if err:
179+
return {"success": False, "message": err}
180+
params["createChild"] = child_params
146181

147182
# Send command to Unity
148183
response = await send_with_unity_instance(

0 commit comments

Comments
 (0)