diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b5386e..ba314e5 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 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 14949a0..f2a09c2 100644 --- a/Editor/MCPServer.Identity.cs +++ b/Editor/MCPServer.Identity.cs @@ -45,29 +45,68 @@ private static string ReadPackageVersion() internal static string AuthToken => EnsureAuthToken(); + 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; + + bool isMainThread = _mainThreadId != -1 && Thread.CurrentThread.ManagedThreadId == _mainThreadId; + + if (isMainThread) + { + try + { + _authToken = SessionState.GetString(AuthSessionStateKey, string.Empty); + } + catch { } + } + + if (string.IsNullOrEmpty(_authToken)) + { + _authToken = ReadTokenFile(); + } - try - { - _authToken = SessionState.GetString(AuthSessionStateKey, string.Empty); if (string.IsNullOrEmpty(_authToken)) { _authToken = Guid.NewGuid().ToString("N") + Guid.NewGuid().ToString("N"); - SessionState.SetString(AuthSessionStateKey, _authToken); + } + + if (isMainThread) + { + try + { + SessionState.SetString(AuthSessionStateKey, _authToken); + } + catch { } } WriteTokenFile(_authToken); + return _authToken; } - catch { } + } - return _authToken; + internal static string RotateAuthToken() + { + lock (AuthTokenLock) + { + _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; + } } private static void WriteTokenFile(string token) @@ -76,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 { } } 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() {