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
1 change: 1 addition & 0 deletions API_REFERENCE.MD
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions DOCUMENTATION.MD
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 1 addition & 6 deletions Editor/MCPServer.Logs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
14 changes: 13 additions & 1 deletion Editor/MCPServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
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<Action> _mainThreadQueue;
private static ConcurrentQueue<LogEntry> _logs;
Expand Down Expand Up @@ -66,6 +67,17 @@

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<Action>();
Expand All @@ -75,7 +87,7 @@
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void RuntimeInit() => Init();

[InitializeOnLoadMethod]

Check warning on line 90 in Editor/MCPServer.cs

View workflow job for this annotation

GitHub Actions / Static validation

NQG012 Init

Method has 58 lines; consider splitting before it exceeds 150.

Check warning on line 90 in Editor/MCPServer.cs

View workflow job for this annotation

GitHub Actions / Documentation quality AI

NQG012 Init

Method has 58 lines; consider splitting before it exceeds 150.
internal static void Init()
{
if (_mainThreadId == -1) _mainThreadId = Thread.CurrentThread.ManagedThreadId;
Expand All @@ -90,7 +102,7 @@
SessionState.SetInt("MCP_SessionGen", SessionGeneration);
}

LastMainThreadTickUtc = DateTime.UtcNow;
RefreshMainThreadCachedState();
#if UNITY_EDITOR_OSX
AppNapBypass.CacheApplicationPath();
#endif
Expand Down Expand Up @@ -159,7 +171,7 @@
/// The method initializes tool dispatch, resolves or allocates the configured port, records restart intent, may attach to an
/// existing Nexus Unity instance on the same port, and enables the macOS App Nap bypass before binding the listener.
/// </remarks>
public static void Start()

Check warning on line 174 in Editor/MCPServer.cs

View workflow job for this annotation

GitHub Actions / Static validation

NQG012 Start

Method has 71 lines; consider splitting before it exceeds 150.

Check warning on line 174 in Editor/MCPServer.cs

View workflow job for this annotation

GitHub Actions / Documentation quality AI

NQG012 Start

Method has 71 lines; consider splitting before it exceeds 150.
{
if (_mainThreadId != -1 && Thread.CurrentThread.ManagedThreadId != _mainThreadId)
{
Expand Down
3 changes: 1 addition & 2 deletions Editor/MCPServerMethods.Status.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using System.Linq;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using Newtonsoft.Json.Linq;

namespace UnityMCP.Editor
Expand Down Expand Up @@ -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 {
Expand Down
10 changes: 6 additions & 4 deletions Editor/MCPServerMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ internal static void Init()
return;
}

MCPServer.RefreshMainThreadCachedState();
NexusEditorLog.Log(NexusLogCategory.Api, "[MCP] MCPServerMethods.Init starting...");
_methods.Clear();
ClearCache();
Expand Down Expand Up @@ -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); }
}
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions Tests~/Editor/AgentToolingTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
Expand Down Expand Up @@ -112,6 +113,35 @@ public void ResetToolUsageStatsClearsPreviousCalls()
Assert.IsFalse(tools.Children<JObject>().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()
{
Expand Down
Loading