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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
62 changes: 51 additions & 11 deletions Editor/MCPServer.Identity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 { }
}
Expand Down
17 changes: 17 additions & 0 deletions Tests~/Editor/ServerPortTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Loading