From fd29719a402590592863266f21efea7ffe55834f Mon Sep 17 00:00:00 2001 From: Daliys Date: Mon, 22 Jun 2026 00:03:51 +0200 Subject: [PATCH] fix: make GameObject read-back honest --- API_REFERENCE.MD | 3 ++ CHANGELOG.md | 3 ++ DOCUMENTATION.MD | 3 ++ Editor/MCPServerMethods.Asset.cs | 5 +++ Editor/MCPServerMethods.Hierarchy.cs | 4 +++ Editor/MCPServerMethods.Reflection.cs | 4 ++- Editor/MCPServerMethods.Status.cs | 16 +++++++--- README.md | 3 ++ Tests~/Editor/ConsolidatedManagersTests.cs | 36 ++++++++++++++++++++++ 9 files changed, 71 insertions(+), 6 deletions(-) diff --git a/API_REFERENCE.MD b/API_REFERENCE.MD index c182c7f..7b0fc74 100644 --- a/API_REFERENCE.MD +++ b/API_REFERENCE.MD @@ -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. diff --git a/CHANGELOG.md b/CHANGELOG.md index cdd008a..cdef4e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/DOCUMENTATION.MD b/DOCUMENTATION.MD index f8fdc85..1606831 100644 --- a/DOCUMENTATION.MD +++ b/DOCUMENTATION.MD @@ -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. diff --git a/Editor/MCPServerMethods.Asset.cs b/Editor/MCPServerMethods.Asset.cs index 6b1fbaf..a5cf9e1 100644 --- a/Editor/MCPServerMethods.Asset.cs +++ b/Editor/MCPServerMethods.Asset.cs @@ -164,6 +164,11 @@ private static Color ParseColorToken(JToken token) 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(); diff --git a/Editor/MCPServerMethods.Hierarchy.cs b/Editor/MCPServerMethods.Hierarchy.cs index 0e27213..82ef1a2 100644 --- a/Editor/MCPServerMethods.Hierarchy.cs +++ b/Editor/MCPServerMethods.Hierarchy.cs @@ -17,6 +17,8 @@ namespace UnityMCP.Editor /// public static partial class MCPServerMethods { + private static DateTime _scriptRefreshBusyUntilUtc; + private static void RegisterHierarchyMethods() { _methods["duplicate_object"] = DuplicateObject; @@ -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 diff --git a/Editor/MCPServerMethods.Reflection.cs b/Editor/MCPServerMethods.Reflection.cs index c03ccdc..c297a0d 100644 --- a/Editor/MCPServerMethods.Reflection.cs +++ b/Editor/MCPServerMethods.Reflection.cs @@ -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().Where(c => c != null).Select(c => SerializeComponentSnapshot(c, false))) }; } } } diff --git a/Editor/MCPServerMethods.Status.cs b/Editor/MCPServerMethods.Status.cs index 9ff5831..78a5ac9 100644 --- a/Editor/MCPServerMethods.Status.cs +++ b/Editor/MCPServerMethods.Status.cs @@ -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) { @@ -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, @@ -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"), diff --git a/README.md b/README.md index 7ac9b44..26365bb 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/Tests~/Editor/ConsolidatedManagersTests.cs b/Tests~/Editor/ConsolidatedManagersTests.cs index 2ffa875..378701c 100644 --- a/Tests~/Editor/ConsolidatedManagersTests.cs +++ b/Tests~/Editor/ConsolidatedManagersTests.cs @@ -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(), 0.001f); + Assert.AreEqual(2f, data["transform"]["position"]["y"].Value(), 0.001f); + Assert.AreEqual(3f, data["transform"]["position"]["z"].Value(), 0.001f); + Assert.AreEqual(10f, data["transform"]["rotation"]["x"].Value(), 0.001f); + Assert.AreEqual(20f, data["transform"]["rotation"]["y"].Value(), 0.001f); + Assert.AreEqual(30f, data["transform"]["rotation"]["z"].Value(), 0.001f); + Assert.AreEqual(2f, data["transform"]["scale"]["x"].Value(), 0.001f); + Assert.AreEqual(3f, data["transform"]["scale"]["y"].Value(), 0.001f); + Assert.AreEqual(4f, data["transform"]["scale"]["z"].Value(), 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();