Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 179 additions & 0 deletions MCPForUnity/Editor/Tools/Prefabs/ManagePrefabs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/// <summary>
/// Creates a single child GameObject within the prefab contents.
/// </summary>
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<bool?>() ?? childParams["set_active"]?.ToObject<bool?>();
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
Expand Down
35 changes: 35 additions & 0 deletions Server/src/services/tools/manage_prefabs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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(
Expand Down
Loading