From 3c9a53f5568e0644ab59b888711a560c218d68f0 Mon Sep 17 00:00:00 2001 From: Daliys Date: Tue, 28 Jul 2026 08:54:44 +0200 Subject: [PATCH 1/2] fix(auth): persist auth token across editor process restarts (#166) --- CHANGELOG.md | 1 + Editor/MCPServer.Identity.cs | 40 +++++++++++++++++++++++++++++++- Tests~/Editor/ServerPortTests.cs | 17 ++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b5386e..dd7b09a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ All notable public changes to Nexus Unity are documented here. - Consolidated duplicate internal Ollama-review and serialized-property write helpers. ### Fixed +- Persist authentication tokens across Unity Editor process restarts in Library and EditorPrefs, preventing persistent HTTP 401 Unauthorized errors for external MCP CLI integrations after restarting Unity (#166). - `get_scene_dependencies` now uses `enterChildren = false` after the initial property iteration step, preventing deep recursive traversal into child properties and eliminating duplicated dependency references (#71). - `find_objects` now safely constructs name search regexes with a 100ms match timeout and falls back gracefully to literal substring search on invalid regex patterns or match timeouts (#119). - Restrict type resolution (`FindType`) to user project assemblies and standard public `UnityEngine` assemblies, disallow internal system/editor assemblies and namespaces, and enforce strict type allowlist constraints for component and ScriptableObject creation/inspection tools (#139). diff --git a/Editor/MCPServer.Identity.cs b/Editor/MCPServer.Identity.cs index 14949a0..d2a5056 100644 --- a/Editor/MCPServer.Identity.cs +++ b/Editor/MCPServer.Identity.cs @@ -45,6 +45,19 @@ private static string ReadPackageVersion() internal static string AuthToken => EnsureAuthToken(); + private static string GetEditorPrefsTokenKey() + { + try + { + string projectRoot = Path.GetFullPath(Path.Combine(UnityEngine.Application.dataPath, "..")); + return "NexusUnity_AuthToken_" + Math.Abs(projectRoot.GetHashCode()); + } + catch + { + return "NexusUnity_AuthToken_Default"; + } + } + private static string EnsureAuthToken() { if (!string.IsNullOrEmpty(_authToken)) return _authToken; @@ -57,12 +70,24 @@ private static string EnsureAuthToken() try { _authToken = SessionState.GetString(AuthSessionStateKey, string.Empty); + + if (string.IsNullOrEmpty(_authToken)) + { + _authToken = ReadTokenFile(); + } + + if (string.IsNullOrEmpty(_authToken)) + { + _authToken = EditorPrefs.GetString(GetEditorPrefsTokenKey(), string.Empty); + } + if (string.IsNullOrEmpty(_authToken)) { _authToken = Guid.NewGuid().ToString("N") + Guid.NewGuid().ToString("N"); - SessionState.SetString(AuthSessionStateKey, _authToken); } + SessionState.SetString(AuthSessionStateKey, _authToken); + EditorPrefs.SetString(GetEditorPrefsTokenKey(), _authToken); WriteTokenFile(_authToken); } catch { } @@ -70,6 +95,19 @@ private static string EnsureAuthToken() return _authToken; } + internal static string RotateAuthToken() + { + _authToken = Guid.NewGuid().ToString("N") + Guid.NewGuid().ToString("N"); + try + { + SessionState.SetString(AuthSessionStateKey, _authToken); + EditorPrefs.SetString(GetEditorPrefsTokenKey(), _authToken); + WriteTokenFile(_authToken); + } + catch { } + return _authToken; + } + private static void WriteTokenFile(string token) { try diff --git a/Tests~/Editor/ServerPortTests.cs b/Tests~/Editor/ServerPortTests.cs index 3de66a5..3e44ef8 100644 --- a/Tests~/Editor/ServerPortTests.cs +++ b/Tests~/Editor/ServerPortTests.cs @@ -58,6 +58,23 @@ public void AuthToken_IsRequiredForNetworkRequests() Assert.IsTrue((bool)InvokePrivateMethod("IsAuthorizedToken", token)); } + [Test] + public void AuthToken_PersistsAcrossSessionStateReset() + { + string initialToken = MCPServer.AuthToken; + Assert.IsFalse(string.IsNullOrEmpty(initialToken), "Auth token should be non-empty"); + + var authTokenField = typeof(MCPServer).GetField("_authToken", BindingFlags.NonPublic | BindingFlags.Static); + authTokenField?.SetValue(null, null); + + var sessionKeyField = typeof(MCPServer).GetField("AuthSessionStateKey", BindingFlags.NonPublic | BindingFlags.Static); + string sessionKey = (string)sessionKeyField?.GetValue(null) ?? "NexusUnity_AuthToken"; + SessionState.SetString(sessionKey, string.Empty); + + string restoredToken = MCPServer.AuthToken; + Assert.AreEqual(initialToken, restoredToken, "AuthToken should persist across SessionState resets"); + } + [Test] public void GetPortOwner_IdentifiesProcess() { From e744784c19b99419f04586c79066c45b53746175 Mon Sep 17 00:00:00 2001 From: Daliys Date: Tue, 28 Jul 2026 11:03:58 +0200 Subject: [PATCH 2/2] fix(auth): thread-safe auth token persistence via Library file (#166) --- CHANGELOG.md | 2 +- Editor/MCPServer.Identity.cs | 70 ++++++++++++++++++------------------ 2 files changed, 37 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd7b09a..ba314e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ All notable public changes to Nexus Unity are documented here. - Consolidated duplicate internal Ollama-review and serialized-property write helpers. ### Fixed -- Persist authentication tokens across Unity Editor process restarts in Library and EditorPrefs, preventing persistent HTTP 401 Unauthorized errors for external MCP CLI integrations after restarting Unity (#166). +- Persist authentication tokens across Unity Editor process restarts in Library token file, preventing persistent HTTP 401 Unauthorized errors for external MCP CLI integrations after restarting Unity (#166). - `get_scene_dependencies` now uses `enterChildren = false` after the initial property iteration step, preventing deep recursive traversal into child properties and eliminating duplicated dependency references (#71). - `find_objects` now safely constructs name search regexes with a 100ms match timeout and falls back gracefully to literal substring search on invalid regex patterns or match timeouts (#119). - Restrict type resolution (`FindType`) to user project assemblies and standard public `UnityEngine` assemblies, disallow internal system/editor assemblies and namespaces, and enforce strict type allowlist constraints for component and ScriptableObject creation/inspection tools (#139). diff --git a/Editor/MCPServer.Identity.cs b/Editor/MCPServer.Identity.cs index d2a5056..f2a09c2 100644 --- a/Editor/MCPServer.Identity.cs +++ b/Editor/MCPServer.Identity.cs @@ -45,40 +45,30 @@ private static string ReadPackageVersion() internal static string AuthToken => EnsureAuthToken(); - private static string GetEditorPrefsTokenKey() - { - try - { - string projectRoot = Path.GetFullPath(Path.Combine(UnityEngine.Application.dataPath, "..")); - return "NexusUnity_AuthToken_" + Math.Abs(projectRoot.GetHashCode()); - } - catch - { - return "NexusUnity_AuthToken_Default"; - } - } + private static readonly object AuthTokenLock = new object(); private static string EnsureAuthToken() { if (!string.IsNullOrEmpty(_authToken)) return _authToken; - if (_mainThreadId != -1 && Thread.CurrentThread.ManagedThreadId != _mainThreadId) + lock (AuthTokenLock) { - return _authToken; - } + if (!string.IsNullOrEmpty(_authToken)) return _authToken; - try - { - _authToken = SessionState.GetString(AuthSessionStateKey, string.Empty); + bool isMainThread = _mainThreadId != -1 && Thread.CurrentThread.ManagedThreadId == _mainThreadId; - if (string.IsNullOrEmpty(_authToken)) + if (isMainThread) { - _authToken = ReadTokenFile(); + try + { + _authToken = SessionState.GetString(AuthSessionStateKey, string.Empty); + } + catch { } } if (string.IsNullOrEmpty(_authToken)) { - _authToken = EditorPrefs.GetString(GetEditorPrefsTokenKey(), string.Empty); + _authToken = ReadTokenFile(); } if (string.IsNullOrEmpty(_authToken)) @@ -86,26 +76,37 @@ private static string EnsureAuthToken() _authToken = Guid.NewGuid().ToString("N") + Guid.NewGuid().ToString("N"); } - SessionState.SetString(AuthSessionStateKey, _authToken); - EditorPrefs.SetString(GetEditorPrefsTokenKey(), _authToken); + if (isMainThread) + { + try + { + SessionState.SetString(AuthSessionStateKey, _authToken); + } + catch { } + } + WriteTokenFile(_authToken); + return _authToken; } - catch { } - - return _authToken; } internal static string RotateAuthToken() { - _authToken = Guid.NewGuid().ToString("N") + Guid.NewGuid().ToString("N"); - try + lock (AuthTokenLock) { - SessionState.SetString(AuthSessionStateKey, _authToken); - EditorPrefs.SetString(GetEditorPrefsTokenKey(), _authToken); + _authToken = Guid.NewGuid().ToString("N") + Guid.NewGuid().ToString("N"); + bool isMainThread = _mainThreadId != -1 && Thread.CurrentThread.ManagedThreadId == _mainThreadId; + if (isMainThread) + { + try + { + SessionState.SetString(AuthSessionStateKey, _authToken); + } + catch { } + } WriteTokenFile(_authToken); + return _authToken; } - catch { } - return _authToken; } private static void WriteTokenFile(string token) @@ -114,10 +115,11 @@ private static void WriteTokenFile(string token) { string projectRoot = Path.GetFullPath(Path.Combine(UnityEngine.Application.dataPath, "..")); string libraryDir = Path.Combine(projectRoot, "Library"); - if (Directory.Exists(libraryDir)) + if (!Directory.Exists(libraryDir)) { - File.WriteAllText(Path.Combine(libraryDir, "NexusUnityAuthToken.txt"), token); + Directory.CreateDirectory(libraryDir); } + File.WriteAllText(Path.Combine(libraryDir, "NexusUnityAuthToken.txt"), token); } catch { } }