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
3 changes: 2 additions & 1 deletion API_REFERENCE.MD
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Schema compatibility notes:
- `update_component` accepts the preferred `properties` object and the legacy `json_data` string form.
- `create_scene` accepts optional `path` and `open_if_exists`, so agents can create or reopen a deterministic scene asset in one call.
- `create_primitive` accepts optional `name`, `parent_id`, `position`, `rotation`, `scale`, and `material_path` for visible non-origin object creation.
- Vector3 fields accept either `{ "x": 0, "y": 1, "z": 0 }` or `[0, 1, 0]`.
- `set_transform` accepts `position`, `rotation` / `eulerAngles`, and `scale` / `localScale`.
- `create_material` accepts optional `path`, `base_color` / `color`, and `emission_color` so callers can create visible materials in a chosen folder.
- `write_file` and `write_files_batch` create missing parent directories after path validation.
Expand Down Expand Up @@ -168,7 +169,7 @@ Actions: `create`, `open`, `save`, `list`. Aliases: `create_scene`, `open_scene`
### `unity_hierarchy_manager`
Actions: `create_empty`, `create_primitive`, `create_hierarchy`, `destroy`, `duplicate`, `rename`, `set_name`, `set_transform`, `set_active`, `set_parent`, `set_sibling_index`.

Aliases: `create`, `create_gameobject`, and `create_game_object` map to `create_empty`. `create_primitive` accepts `name`, `parent_id`, `position`, `rotation`, `scale`, and `material_path`.
Aliases: `create`, `create_gameobject`, and `create_game_object` map to `create_empty`. `create_primitive` accepts `name`, `parent_id`, `position`, `rotation`, `scale`, and `material_path`; Vector3 fields accept either object or array form.

### `unity_component_manager`
Actions: `add`, `remove`, `inspect`, `get_schema`, `update_properties`, `set_property`, `set_enabled`.
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ All notable public changes to Nexus Unity are documented here.
### Changed
- Relicensed Nexus Unity from `GPL-3.0-only` to the `MIT` license to remove copyleft friction for commercial Unity studios. All prior contributors consented to the relicense.

### Fixed
- `create_primitive` now validates parent, transform, and material inputs before creating the GameObject, and Vector3 inputs accept `[x, y, z]` arrays as well as `{x, y, z}` objects.

## [1.4.2] - 2026-06-13

### Added
Expand Down
1 change: 1 addition & 0 deletions DOCUMENTATION.MD
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ The current development line preserves backward compatibility while correcting p
- `update_component` should use a `properties` object for new callers; `json_data` remains accepted for existing callers.
- `create_scene` accepts optional `path` and `open_if_exists` for deterministic scene assets.
- `create_primitive` accepts optional `name`, `parent_id`, `position`, `rotation`, `scale`, and `material_path`.
- Vector3 fields accept either `{ "x": 0, "y": 1, "z": 0 }` or `[0, 1, 0]`.
- `set_transform` accepts `position`, `rotation` / `eulerAngles`, and `scale` / `localScale`.
- `create_material` accepts an optional explicit `path` plus `base_color` / `color` and `emission_color`, so automation can isolate generated materials in a known project folder and make them visibly distinct.
- `write_file` and `write_files_batch` create missing parent directories after path validation.
Expand Down
49 changes: 34 additions & 15 deletions Editor/MCPServerMethods.Core.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,37 +63,56 @@ private static JToken CreatePrimitive(JToken p)
{
if (p == null || p["primitive_type"] == null) throw new Exception("primitive_type is required");
if (!Enum.TryParse(typeof(PrimitiveType), p["primitive_type"].ToString(), true, out var type)) throw new Exception("Invalid primitive");
var go = GameObject.CreatePrimitive((PrimitiveType)type);
Undo.RegisterCreatedObjectUndo(go, "Create Primitive");

string name = p["name"]?.ToString();
if (!string.IsNullOrWhiteSpace(name)) go.name = name;

Transform parentTransform = null;
if (p["parent_id"] != null)
{
var parent = MCPServerMethods.IdToObject(MCPServerMethods.ExtractId(p, "parent_id")) as GameObject;
if (parent == null) throw new Exception("parent_id does not resolve to a GameObject");
go.transform.SetParent(parent.transform, false);
parentTransform = parent.transform;
}

if (p["position"] != null) go.transform.position = ParseVector3(p["position"], go.transform.position);
JToken rotation = p["rotation"] ?? p["eulerAngles"];
if (rotation != null) go.transform.eulerAngles = ParseVector3(rotation, go.transform.eulerAngles);
JToken scale = p["scale"] ?? p["localScale"];
if (scale != null) go.transform.localScale = ParseVector3(scale, go.transform.localScale);
Vector3? position = p["position"] != null ? ParseVector3(p["position"]) : null;
JToken rotationToken = p["rotation"] ?? p["eulerAngles"];
Vector3? rotation = rotationToken != null ? ParseVector3(rotationToken) : null;
JToken scaleToken = p["scale"] ?? p["localScale"];
Vector3? scale = scaleToken != null ? ParseVector3(scaleToken) : null;

Material material = null;
string materialPath = p["material_path"]?.ToString();
if (!string.IsNullOrWhiteSpace(materialPath))
{
materialPath = ValidateAssetPath(materialPath);
var material = AssetDatabase.LoadAssetAtPath<Material>(materialPath);
material = AssetDatabase.LoadAssetAtPath<Material>(materialPath);
if (material == null) throw new Exception($"Material not found at {materialPath}");
var renderer = go.GetComponent<Renderer>();
if (renderer != null) renderer.sharedMaterial = material;
}

Selection.activeGameObject = go;
return new JObject { ["status"] = "Success", ["data"] = SerializeGameObject(go) };
GameObject go = null;
try
{
go = GameObject.CreatePrimitive((PrimitiveType)type);
Undo.RegisterCreatedObjectUndo(go, "Create Primitive");

if (!string.IsNullOrWhiteSpace(name)) go.name = name;

if (parentTransform != null) go.transform.SetParent(parentTransform, false);

if (position.HasValue) go.transform.position = position.Value;
if (rotation.HasValue) go.transform.eulerAngles = rotation.Value;
if (scale.HasValue) go.transform.localScale = scale.Value;

var renderer = go.GetComponent<Renderer>();
if (renderer != null && material != null) renderer.sharedMaterial = material;

Selection.activeGameObject = go;
return new JObject { ["status"] = "Success", ["data"] = SerializeGameObject(go) };
}
catch
{
if (go != null) UnityEngine.Object.DestroyImmediate(go);
throw;
}
}

private static JToken AttachScript(JToken p)
Expand Down
6 changes: 6 additions & 0 deletions Editor/MCPServerMethods.Reflection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,12 @@ private static JToken FormatResult(MethodInfo method, object result)
private static Vector3 ParseVector3(JToken t, Vector3 _defaultValue = default)
{
if (t == null) return _defaultValue;
if (t is JArray array)
{
if (array.Count != 3) throw new Exception("Vector3 array must have exactly 3 numbers");
return new Vector3(array[0].Value<float>(), array[1].Value<float>(), array[2].Value<float>());
}
if (t.Type != JTokenType.Object) throw new Exception("Vector3 must be an object with x/y/z or an array [x,y,z]");
return new Vector3((float)(t["x"] ?? _defaultValue.x), (float)(t["y"] ?? _defaultValue.y), (float)(t["z"] ?? _defaultValue.z));
}

Expand Down
25 changes: 18 additions & 7 deletions Editor/MCPServerMethods.Tools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -397,13 +397,24 @@ private static JObject GetPrimitiveSchema()

private static JObject GetVector3Schema() => new JObject
{
["type"] = "object",
["properties"] = new JObject
{
["x"] = new JObject { ["type"] = "number" },
["y"] = new JObject { ["type"] = "number" },
["z"] = new JObject { ["type"] = "number" }
}
["oneOf"] = new JArray(
new JObject
{
["type"] = "object",
["properties"] = new JObject
{
["x"] = new JObject { ["type"] = "number" },
["y"] = new JObject { ["type"] = "number" },
["z"] = new JObject { ["type"] = "number" }
}
},
new JObject
{
["type"] = "array",
["items"] = new JObject { ["type"] = "number" },
["minItems"] = 3,
["maxItems"] = 3
})
};

private static string SanitizeScriptName(string n) => System.Text.RegularExpressions.Regex.Replace(n, @"[^a-zA-Z0-9_]", "_");
Expand Down
22 changes: 16 additions & 6 deletions Editor/nexus_bridge/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,22 @@

# --- Shared sub-schemas ---
VECTOR3_SCHEMA: JsonObject = {
"type": "object",
"properties": {
"x": {"type": "number"},
"y": {"type": "number"},
"z": {"type": "number"},
},
"oneOf": [
{
"type": "object",
"properties": {
"x": {"type": "number"},
"y": {"type": "number"},
"z": {"type": "number"},
},
},
{
"type": "array",
"items": {"type": "number"},
"minItems": 3,
"maxItems": 3,
},
],
}

STATIC_TOOLS: list[ToolDefinition] = [
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ Current development keeps the public API backward-compatible while tightening sc
- `unity_scene_manager` accepts obvious aliases such as `list_scenes`, `create_scene`, `open_scene`, and `save_scene`; invalid manager actions now report the valid action names.
- `create_scene` accepts optional `path` and `open_if_exists` so agents can create or reopen a deterministic scene asset in one call.
- `create_primitive` accepts optional `name`, `parent_id`, `position`, `rotation`, `scale`, and `material_path`, which lets agents build visible non-origin objects without fragile follow-up calls.
- Vector3 fields accept either `{ "x": 0, "y": 1, "z": 0 }` or `[0, 1, 0]`.
- `set_transform` updates position, rotation, and scale.
- `create_material` accepts optional `path`, `base_color` / `color`, and `emission_color` so generated materials can be created inside a chosen project folder and made visibly distinct.
- `write_file` and `write_files_batch` create missing parent directories after path validation.
Expand Down
38 changes: 38 additions & 0 deletions Tests~/Editor/ConsolidatedManagersTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,44 @@ public void UnityHierarchyManager_CreatePrimitiveWithNameAndTransform_ReturnsPla
Assert.AreEqual(2f, go.transform.localScale.z, 0.001f);
}

[Test]
public void UnityHierarchyManager_CreatePrimitiveWithArrayPosition_ReturnsPlacedObject() {
var res = SimulateBridgeRouting("unity_hierarchy_manager", new JObject {
["action"] = "create_primitive",
["primitive_type"] = "Cube",
["name"] = "ArrayPlacedManagerCube",
["position"] = new JArray(9, 9, 9)
});

Assert.IsNotNull(res["result"], $"Expected result, got error: {res["error"]}");
var id = res["result"]["data"]?["instance_id"]?.Value<int>();
Assert.IsTrue(id.HasValue && id.Value != 0);

var go = EditorUtility.InstanceIDToObject(id.Value) as GameObject;
Assert.IsNotNull(go);
_createdObjects.Add(go);
Assert.AreEqual(9f, go.transform.position.x, 0.001f);
Assert.AreEqual(9f, go.transform.position.y, 0.001f);
Assert.AreEqual(9f, go.transform.position.z, 0.001f);
}

[Test]
public void UnityHierarchyManager_CreatePrimitiveWithInvalidArrayPosition_DoesNotCreateObject() {
string name = "InvalidArrayCube_" + Guid.NewGuid().ToString("N");
int before = GameObject.FindObjectsOfType<GameObject>().Count(go => go.name == name);

var res = SimulateBridgeRouting("unity_hierarchy_manager", new JObject {
["action"] = "create_primitive",
["primitive_type"] = "Cube",
["name"] = name,
["position"] = new JArray(9, 9)
});

Assert.IsNotNull(res["error"], "Expected invalid Vector3 array to fail.");
int after = GameObject.FindObjectsOfType<GameObject>().Count(go => go.name == name);
Assert.AreEqual(before, after);
}

[Test]
public void UnityHierarchyManager_RenameAlias_UpdatesGameObjectName() {
var go = CreateTestGameObject();
Expand Down
Loading