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: 3 additions & 0 deletions API_REFERENCE.MD
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ Schema compatibility notes:
- `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`.
- `get_game_object` returns `transform.position`, `transform.rotation`, `transform.scale`, and compact `components` data for cheap read-back verification.
- After script writes, readiness probes remain busy until the scheduled asset refresh window has passed.
- During Play Mode transitions, readiness probes remain busy until Unity finishes entering or exiting Play Mode.
- `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.
- `invoke_method.arguments` is an optional JSON array of positional arguments.
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ All notable public changes to Nexus Unity are documented here.

### 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.
- `get_game_object` now returns transform state and a compact component list so agents can verify basic write operations with the cheap read-back call.
- Script writes now keep readiness probes in a busy/importing state while the scheduled asset refresh is pending, avoiding premature follow-up write calls during Unity domain reload.
- Play mode transitions now keep readiness probes busy so agents do not issue follow-up writes while Unity is still entering or exiting Play Mode.

## [1.4.2] - 2026-06-13

Expand Down
3 changes: 3 additions & 0 deletions DOCUMENTATION.MD
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ The current development line preserves backward compatibility while correcting p
- `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`.
- `get_game_object` includes transform state and a compact component list for cheap write verification; use `inspect_component` for full component properties.
- Readiness probes remain busy while a script write's scheduled asset refresh is pending, so agents do not race Unity domain reload.
- Play Mode transitions keep readiness probes busy until Unity finishes entering or exiting Play Mode.
- `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.
- `invoke_method.arguments` is documented and exposed as a JSON array schema.
Expand Down
5 changes: 5 additions & 0 deletions Editor/MCPServerMethods.Asset.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;

Check warning on line 1 in Editor/MCPServerMethods.Asset.cs

View workflow job for this annotation

GitHub Actions / Static validation

NQG002

File has 301 lines; consider splitting before it exceeds 450.

Check warning on line 1 in Editor/MCPServerMethods.Asset.cs

View workflow job for this annotation

GitHub Actions / Documentation quality AI

NQG002

File has 301 lines; consider splitting before it exceeds 450.
using System.IO;
using System.Linq;
using UnityEditor;
Expand Down Expand Up @@ -164,6 +164,11 @@

private static JToken RefreshAssetDatabase(JToken p)
{
if (DateTime.UtcNow < _scriptRefreshBusyUntilUtc)
{
return new JObject { ["status"] = "Compiling", ["is_compiling"] = false, ["is_updating"] = true, ["compiler_errors"] = new JArray() };
}

#if UNITY_EDITOR_OSX
// Signal AppNapBypass to bring Unity to the foreground so compilation can happen.
AppNapBypass.ScheduleActivation();
Expand Down
4 changes: 4 additions & 0 deletions Editor/MCPServerMethods.Hierarchy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ namespace UnityMCP.Editor
/// </remarks>
public static partial class MCPServerMethods
{
private static DateTime _scriptRefreshBusyUntilUtc;

private static void RegisterHierarchyMethods()
{
_methods["duplicate_object"] = DuplicateObject;
Expand Down Expand Up @@ -288,6 +290,8 @@ private static void EnsureParentDirectory(string fullPath)

private static void TriggerSafeAssetRefresh()
{
_scriptRefreshBusyUntilUtc = DateTime.UtcNow.AddSeconds(8);

// Scripts trigger domain reload which blocks the HTTP response.
// Unity strictly aborts Domain Reloads if the Editor window is in the background
// to protect external IDE development. We use LaunchServices (open -a) to explicitly
Expand Down
4 changes: 3 additions & 1 deletion Editor/MCPServerMethods.Reflection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,9 @@ private static Vector3 ParseVector3(JToken t, Vector3 _defaultValue = default)
private static JToken SerializeGameObject(GameObject go)
{
if (go == null) return JValue.CreateNull();
return new JObject { ["name"] = go.name, ["instance_id"] = go.GetRawId() };
return new JObject { ["name"] = go.name, ["instance_id"] = go.GetRawId(),
["transform"] = new JObject { ["position"] = SerializeVector3(go.transform.position), ["rotation"] = SerializeVector3(go.transform.eulerAngles), ["scale"] = SerializeVector3(go.transform.localScale) },
["components"] = new JArray(go.GetComponents<Component>().Where(c => c != null).Select(c => SerializeComponentSnapshot(c, false))) };
}
}
}
16 changes: 11 additions & 5 deletions Editor/MCPServerMethods.Status.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ private static JToken ShutdownServer(JToken p)
return new JObject { ["status"] = "Shutting down..." };
}

private static JToken Initialize(JToken p) => new JObject { ["protocolVersion"] = "2024-11-05", ["serverInfo"] = new JObject { ["name"] = "Unity MCP Server", ["version"] = MCPServer.Version } };
private static JToken Initialize(JToken p)
{
if (MCPServer.IsCompilingCached || MCPServer.IsUpdatingCached || MCPServer.IsPlayModeTransitionCached || DateTime.UtcNow < _scriptRefreshBusyUntilUtc)
throw new Exception("Unity editor is busy compiling, importing assets, or changing play mode.");
return new JObject { ["protocolVersion"] = "2024-11-05", ["serverInfo"] = new JObject { ["name"] = "Unity MCP Server", ["version"] = MCPServer.Version } };
}

private static JToken GetServerStatus(JToken p)
{
Expand All @@ -27,12 +32,13 @@ private static JToken GetServerStatus(JToken p)
bool isPlaying = MCPServer.IsPlayingCached;
bool isPaused = MCPServer.IsPausedCached;
bool isPlayModeTransition = MCPServer.IsPlayModeTransitionCached;
bool isScriptRefreshPending = DateTime.UtcNow < _scriptRefreshBusyUntilUtc;
bool isMainThreadResponsive = (DateTime.UtcNow - MCPServer.LastMainThreadTickUtc).TotalSeconds < 5;

string busyReason = "idle";
if (isCompiling) busyReason = "compiling";
else if (isUpdating) busyReason = "importing";
else if (isPlayModeTransition && !isPlaying) busyReason = "play_mode_transition";
else if (isUpdating || isScriptRefreshPending) busyReason = "importing";
else if (isPlayModeTransition) busyReason = "play_mode_transition";

return new JObject {
["serverAlive"] = MCPServer.IsRunning,
Expand All @@ -49,13 +55,13 @@ private static JToken GetServerStatus(JToken p)
["editorState"] = new JObject {
["isPlaying"] = isPlaying,
["isCompiling"] = isCompiling,
["isImporting"] = isUpdating,
["isImporting"] = isUpdating || isScriptRefreshPending,
["isPaused"] = isPaused,
["isPlayModeTransition"] = isPlayModeTransition
},
["commandState"] = new JObject {
["acceptsReadCommands"] = true,
["acceptsWriteCommands"] = !isCompiling && !isUpdating,
["acceptsWriteCommands"] = !isCompiling && !isUpdating && !isPlayModeTransition && !isScriptRefreshPending,
["busyReason"] = busyReason
},
["lastHeartbeatUtc"] = MCPServer.LastMainThreadTickUtc.ToString("o"),
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ Current development keeps the public API backward-compatible while tightening sc
- `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.
- `get_game_object` returns `transform` and compact `components` data for cheap verify-after-write reads.
- Script writes keep readiness probes busy while Unity's scheduled refresh is pending.
- Play Mode transitions keep readiness probes busy until Unity finishes entering or exiting Play Mode.
- `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.
- `invoke_method.arguments` is an optional positional JSON array.
Expand Down
36 changes: 36 additions & 0 deletions Tests~/Editor/ConsolidatedManagersTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,42 @@ public void UnityHierarchyManager_SetTransform_UpdatesPositionRotationAndScale()
Assert.AreEqual(4f, go.transform.localScale.z, 0.001f);
}

[Test]
public void GetGameObject_ReturnsTransformAndComponentsForReadBackVerification() {
var go = CreateTestGameObject();

var transformRes = SimulateBridgeRouting("unity_hierarchy_manager", new JObject {
["action"] = "set_transform",
["instance_id"] = go.GetInstanceID(),
["position"] = new JObject { ["x"] = -1, ["y"] = 2, ["z"] = 3 },
["rotation"] = new JObject { ["x"] = 10, ["y"] = 20, ["z"] = 30 },
["scale"] = new JObject { ["x"] = 2, ["y"] = 3, ["z"] = 4 }
});
Assert.IsNotNull(transformRes["result"], $"Expected result, got error: {transformRes["error"]}");

var addComponentRes = CallRaw("add_component", new JObject { ["instance_id"] = go.GetInstanceID(), ["component_name"] = "BoxCollider" });
Assert.IsNotNull(addComponentRes["result"], $"Expected result, got error: {addComponentRes["error"]}");

var readRes = CallRaw("get_game_object", new JObject { ["instance_id"] = go.GetInstanceID() });
Assert.IsNotNull(readRes["result"], $"Expected result, got error: {readRes["error"]}");
var data = readRes["result"]["data"];
Assert.IsNotNull(data);
Assert.AreEqual(-1f, data["transform"]["position"]["x"].Value<float>(), 0.001f);
Assert.AreEqual(2f, data["transform"]["position"]["y"].Value<float>(), 0.001f);
Assert.AreEqual(3f, data["transform"]["position"]["z"].Value<float>(), 0.001f);
Assert.AreEqual(10f, data["transform"]["rotation"]["x"].Value<float>(), 0.001f);
Assert.AreEqual(20f, data["transform"]["rotation"]["y"].Value<float>(), 0.001f);
Assert.AreEqual(30f, data["transform"]["rotation"]["z"].Value<float>(), 0.001f);
Assert.AreEqual(2f, data["transform"]["scale"]["x"].Value<float>(), 0.001f);
Assert.AreEqual(3f, data["transform"]["scale"]["y"].Value<float>(), 0.001f);
Assert.AreEqual(4f, data["transform"]["scale"]["z"].Value<float>(), 0.001f);

var components = data["components"] as JArray;
Assert.IsNotNull(components);
Assert.IsTrue(components.Any(c => c["type"]?.ToString() == "Transform"));
Assert.IsTrue(components.Any(c => c["type"]?.ToString() == "BoxCollider"));
}

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