diff --git a/API_REFERENCE.MD b/API_REFERENCE.MD index 352ea1b..50cec84 100644 --- a/API_REFERENCE.MD +++ b/API_REFERENCE.MD @@ -28,6 +28,7 @@ Schema compatibility notes: - 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. +- Fast-path health calls (`get_server_status`, `attach_existing_session`, `wait_for_asset_import_idle`, and `wait_for_editor_idle`) use cached editor state because they may run on the listener thread. - 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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 547b112..ebcd5ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ All notable public changes to Nexus Unity are documented here. - Path security tests now exercise JSON-RPC traversal rejection for representative file and asset tools. - `get_test_results` now reads messages only from scoped NUnit result nodes and falls back to the test result instead of unrelated nested messages. - `unity_scene_manager` schema aliases now preserve action-specific required parameters. +- Fast-path health JSON-RPC methods now use cached editor state instead of direct Unity API reads on the listener thread. ## [1.4.2] - 2026-06-13 diff --git a/DOCUMENTATION.MD b/DOCUMENTATION.MD index 9c26372..7531275 100644 --- a/DOCUMENTATION.MD +++ b/DOCUMENTATION.MD @@ -24,6 +24,7 @@ The server: - Restarts after Unity domain reloads when the previous session was running. - Tracks session id, session generation, last heartbeat, play mode, compile/import state, and last error. - Provides `get_server_status`, `ping_main_thread`, `wait_for_asset_import_idle`, `wait_for_editor_idle`, `get_test_results`, `get_tool_usage_stats`, and `reset_tool_usage_stats` for robust automation loops. +- Serves fast-path health and wait calls from cached editor state when they run on the listener thread. ## Unity Editor Menu diff --git a/Editor/MCPServer.Logs.cs b/Editor/MCPServer.Logs.cs index 46acafe..f2d263c 100644 --- a/Editor/MCPServer.Logs.cs +++ b/Editor/MCPServer.Logs.cs @@ -108,12 +108,7 @@ private static void SyncWithUnityConsole() internal static void HandleMainThreadQueue() { - LastMainThreadTickUtc = DateTime.UtcNow; - IsCompilingCached = EditorApplication.isCompiling; - IsUpdatingCached = EditorApplication.isUpdating; - IsPlayingCached = EditorApplication.isPlaying; - IsPausedCached = EditorApplication.isPaused; - IsPlayModeTransitionCached = EditorApplication.isPlayingOrWillChangePlaymode; + RefreshMainThreadCachedState(); if (_mainThreadQueue == null || _mainThreadQueue.IsEmpty) return; diff --git a/Editor/MCPServer.cs b/Editor/MCPServer.cs index 79ad8b9..d86a886 100644 --- a/Editor/MCPServer.cs +++ b/Editor/MCPServer.cs @@ -37,6 +37,7 @@ public static partial class MCPServer public static bool IsPlayingCached { get; private set; } public static bool IsPausedCached { get; private set; } public static bool IsPlayModeTransitionCached { get; private set; } + public static string UnityVersionCached { get; private set; } private static ConcurrentQueue _mainThreadQueue; private static ConcurrentQueue _logs; @@ -66,6 +67,17 @@ private static string GetDeterministicProjectKey() private static readonly object _startLock = new object(); + internal static void RefreshMainThreadCachedState() + { + LastMainThreadTickUtc = DateTime.UtcNow; + IsCompilingCached = EditorApplication.isCompiling; + IsUpdatingCached = EditorApplication.isUpdating; + IsPlayingCached = EditorApplication.isPlaying; + IsPausedCached = EditorApplication.isPaused; + IsPlayModeTransitionCached = EditorApplication.isPlayingOrWillChangePlaymode; + UnityVersionCached = Application.unityVersion; + } + static MCPServer() { _mainThreadQueue = new ConcurrentQueue(); @@ -90,7 +102,7 @@ internal static void Init() SessionState.SetInt("MCP_SessionGen", SessionGeneration); } - LastMainThreadTickUtc = DateTime.UtcNow; + RefreshMainThreadCachedState(); #if UNITY_EDITOR_OSX AppNapBypass.CacheApplicationPath(); #endif diff --git a/Editor/MCPServerMethods.Status.cs b/Editor/MCPServerMethods.Status.cs index 78a5ac9..98544f9 100644 --- a/Editor/MCPServerMethods.Status.cs +++ b/Editor/MCPServerMethods.Status.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Collections.Generic; using UnityEditor; -using UnityEngine; using Newtonsoft.Json.Linq; namespace UnityMCP.Editor @@ -49,7 +48,7 @@ private static JToken GetServerStatus(JToken p) ["sessionId"] = MCPServer.SessionId, ["processId"] = System.Diagnostics.Process.GetCurrentProcess().Id, ["projectPath"] = Directory.GetCurrentDirectory().Replace("\\", "/"), - ["unityVersion"] = Application.unityVersion, + ["unityVersion"] = MCPServer.UnityVersionCached, ["editorConnected"] = true, ["mainThreadResponsive"] = isMainThreadResponsive, ["editorState"] = new JObject { diff --git a/Editor/MCPServerMethods.cs b/Editor/MCPServerMethods.cs index aca83fb..8c1fb50 100644 --- a/Editor/MCPServerMethods.cs +++ b/Editor/MCPServerMethods.cs @@ -40,6 +40,7 @@ internal static void Init() return; } + MCPServer.RefreshMainThreadCachedState(); NexusEditorLog.Log(NexusLogCategory.Api, "[MCP] MCPServerMethods.Init starting..."); _methods.Clear(); ClearCache(); @@ -113,10 +114,11 @@ private static string ProcessJsonRequest(JObject request) string method = request["method"]?.ToString(); if (method == null) return CreateErrorResponse(id, -32600, "Method missing"); - // Fast-path for health checks (execute on current thread, usually listener thread) + // Fast-path health checks run on the listener/current thread. + // Handlers here must only read cached or thread-safe process state. if (method == "get_server_status" || method == "attach_existing_session" || method == "wait_for_asset_import_idle" || method == "wait_for_editor_idle") { try { - JToken result = ExecuteMethod(method, request["params"]); + JToken result = ExecuteMethod(method, request["params"], false); return CreateJsonResponse(id, result); } catch(Exception e) { return CreateExceptionResponse(id, e); } } @@ -215,9 +217,9 @@ private static string CreateExceptionResponse(JToken id, Exception e) return CreateErrorResponse(id, -32000, actualException.Message, stackTrace); } - private static JToken ExecuteMethod(string method, JToken p) + private static JToken ExecuteMethod(string method, JToken p, bool logExecution = true) { - NexusEditorLog.Log(NexusLogCategory.Api, $"[MCP_EXECUTE] {method}"); + if (logExecution) NexusEditorLog.Log(NexusLogCategory.Api, $"[MCP_EXECUTE] {method}"); var stopwatch = System.Diagnostics.Stopwatch.StartNew(); Exception failure = null; try diff --git a/README.md b/README.md index e54aa6d..702ce9d 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,8 @@ C# script writes are guarded: `attach_script`, `write_file`, `write_files_batch` Raw `batch_execute` is capped at 50 requests and rejects nested `batch_execute` calls. +Fast-path health calls (`get_server_status`, `attach_existing_session`, `wait_for_asset_import_idle`, and `wait_for_editor_idle`) return cached editor state so they can run safely from the listener thread. + Direct JSON-RPC example: ```bash diff --git a/Tests~/Editor/AgentToolingTests.cs b/Tests~/Editor/AgentToolingTests.cs index 85b6211..2c94f27 100644 --- a/Tests~/Editor/AgentToolingTests.cs +++ b/Tests~/Editor/AgentToolingTests.cs @@ -1,5 +1,6 @@ using System.IO; using System.Linq; +using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NUnit.Framework; @@ -112,6 +113,35 @@ public void ResetToolUsageStatsClearsPreviousCalls() Assert.IsFalse(tools.Children().Any(t => t["method"]?.ToString() == "get_editor_state")); } + [Test] + public void FastPathMethodsCanRunOffMainThread() + { + NexusConsoleLogMode originalLogMode = MCPSettings.ConsoleLogMode; + string[] methods = + { + "get_server_status", + "attach_existing_session", + "wait_for_asset_import_idle", + "wait_for_editor_idle" + }; + + try + { + MCPSettings.ConsoleLogMode = NexusConsoleLogMode.All; + + foreach (string method in methods) + { + JObject response = Task.Run(() => Rpc(method, new JObject { ["timeout_seconds"] = 0 })).GetAwaiter().GetResult(); + + Assert.IsNull(response["error"], $"{method}: {response.ToString(Formatting.None)}"); + } + } + finally + { + MCPSettings.ConsoleLogMode = originalLogMode; + } + } + [Test] public void BatchExecuteRejectsOversizedBatches() {