diff --git a/UnityMcpBridge/Editor/Data/DefaultServerConfig.cs.meta b/UnityMcpBridge/Editor/Data/DefaultServerConfig.cs.meta
index 6df0a87..82e437f 100644
--- a/UnityMcpBridge/Editor/Data/DefaultServerConfig.cs.meta
+++ b/UnityMcpBridge/Editor/Data/DefaultServerConfig.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: de8f5721c34f7194392e9d8c7d0226c0
\ No newline at end of file
+guid: de8f5721c34f7194392e9d8c7d0226c0
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Editor/Data/McpClients.cs.meta b/UnityMcpBridge/Editor/Data/McpClients.cs.meta
index 3c8449a..e5a1081 100644
--- a/UnityMcpBridge/Editor/Data/McpClients.cs.meta
+++ b/UnityMcpBridge/Editor/Data/McpClients.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: 711b86bbc1f661e4fb2c822e14970e16
\ No newline at end of file
+guid: 711b86bbc1f661e4fb2c822e14970e16
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs.meta b/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs.meta
index d8df968..9eb69d0 100644
--- a/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs.meta
+++ b/UnityMcpBridge/Editor/Helpers/GameObjectSerializer.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: 64b8ff807bc9a401c82015cbafccffac
\ No newline at end of file
+guid: 64b8ff807bc9a401c82015cbafccffac
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Editor/Helpers/PortManager.cs b/UnityMcpBridge/Editor/Helpers/PortManager.cs
index 8e368a6..900cbd9 100644
--- a/UnityMcpBridge/Editor/Helpers/PortManager.cs
+++ b/UnityMcpBridge/Editor/Helpers/PortManager.cs
@@ -2,6 +2,9 @@ using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading;
using Newtonsoft.Json;
using UnityEngine;
@@ -31,15 +34,28 @@ namespace UnityMcpBridge.Editor.Helpers
/// Port number to use
public static int GetPortWithFallback()
{
- // Try to load stored port first
- int storedPort = LoadStoredPort();
- if (storedPort > 0 && IsPortAvailable(storedPort))
+ // Try to load stored port first, but only if it's from the current project
+ var storedConfig = GetStoredPortConfig();
+ if (storedConfig != null &&
+ storedConfig.unity_port > 0 &&
+ storedConfig.project_path == Application.dataPath &&
+ IsPortAvailable(storedConfig.unity_port))
{
- Debug.Log($"Using stored port {storedPort}");
- return storedPort;
+ Debug.Log($"Using stored port {storedConfig.unity_port} for current project");
+ return storedConfig.unity_port;
}
- // If no stored port or stored port is unavailable, find a new one
+ // If stored port exists but is currently busy, wait briefly for release
+ if (storedConfig != null && storedConfig.unity_port > 0)
+ {
+ if (WaitForPortRelease(storedConfig.unity_port, 1500))
+ {
+ Debug.Log($"Stored port {storedConfig.unity_port} became available after short wait");
+ return storedConfig.unity_port;
+ }
+ }
+
+ // If no valid stored port, find a new one and save it
int newPort = FindAvailablePort();
SavePort(newPort);
return newPort;
@@ -86,7 +102,7 @@ namespace UnityMcpBridge.Editor.Helpers
}
///
- /// Check if a specific port is available
+ /// Check if a specific port is available for binding
///
/// Port to check
/// True if port is available
@@ -105,6 +121,61 @@ namespace UnityMcpBridge.Editor.Helpers
}
}
+ ///
+ /// Check if a port is currently being used by Unity MCP Bridge
+ /// This helps avoid unnecessary port changes when Unity itself is using the port
+ ///
+ /// Port to check
+ /// True if port appears to be used by Unity MCP
+ public static bool IsPortUsedByUnityMcp(int port)
+ {
+ try
+ {
+ // Try to make a quick connection to see if it's a Unity MCP server
+ using var client = new TcpClient();
+ var connectTask = client.ConnectAsync(IPAddress.Loopback, port);
+ if (connectTask.Wait(100)) // 100ms timeout
+ {
+ // If connection succeeded, it's likely the Unity MCP server
+ return client.Connected;
+ }
+ return false;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Wait for a port to become available for a limited amount of time.
+ /// Used to bridge the gap during domain reload when the old listener
+ /// hasn't released the socket yet.
+ ///
+ private static bool WaitForPortRelease(int port, int timeoutMs)
+ {
+ int waited = 0;
+ const int step = 100;
+ while (waited < timeoutMs)
+ {
+ if (IsPortAvailable(port))
+ {
+ return true;
+ }
+
+ // If the port is in use by an MCP instance, continue waiting briefly
+ if (!IsPortUsedByUnityMcp(port))
+ {
+ // In use by something else; don't keep waiting
+ return false;
+ }
+
+ Thread.Sleep(step);
+ waited += step;
+ }
+ return IsPortAvailable(port);
+ }
+
///
/// Save port to persistent storage
///
@@ -123,7 +194,7 @@ namespace UnityMcpBridge.Editor.Helpers
string registryDir = GetRegistryDirectory();
Directory.CreateDirectory(registryDir);
- string registryFile = Path.Combine(registryDir, RegistryFileName);
+ string registryFile = GetRegistryFilePath();
string json = JsonConvert.SerializeObject(portConfig, Formatting.Indented);
File.WriteAllText(registryFile, json);
@@ -143,11 +214,17 @@ namespace UnityMcpBridge.Editor.Helpers
{
try
{
- string registryFile = Path.Combine(GetRegistryDirectory(), RegistryFileName);
+ string registryFile = GetRegistryFilePath();
if (!File.Exists(registryFile))
{
- return 0;
+ // Backwards compatibility: try the legacy file name
+ string legacy = Path.Combine(GetRegistryDirectory(), RegistryFileName);
+ if (!File.Exists(legacy))
+ {
+ return 0;
+ }
+ registryFile = legacy;
}
string json = File.ReadAllText(registryFile);
@@ -170,11 +247,17 @@ namespace UnityMcpBridge.Editor.Helpers
{
try
{
- string registryFile = Path.Combine(GetRegistryDirectory(), RegistryFileName);
+ string registryFile = GetRegistryFilePath();
if (!File.Exists(registryFile))
{
- return null;
+ // Backwards compatibility: try the legacy file
+ string legacy = Path.Combine(GetRegistryDirectory(), RegistryFileName);
+ if (!File.Exists(legacy))
+ {
+ return null;
+ }
+ registryFile = legacy;
}
string json = File.ReadAllText(registryFile);
@@ -191,5 +274,33 @@ namespace UnityMcpBridge.Editor.Helpers
{
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".unity-mcp");
}
+
+ private static string GetRegistryFilePath()
+ {
+ string dir = GetRegistryDirectory();
+ string hash = ComputeProjectHash(Application.dataPath);
+ string fileName = $"unity-mcp-port-{hash}.json";
+ return Path.Combine(dir, fileName);
+ }
+
+ private static string ComputeProjectHash(string input)
+ {
+ try
+ {
+ using SHA1 sha1 = SHA1.Create();
+ byte[] bytes = Encoding.UTF8.GetBytes(input ?? string.Empty);
+ byte[] hashBytes = sha1.ComputeHash(bytes);
+ var sb = new StringBuilder();
+ foreach (byte b in hashBytes)
+ {
+ sb.Append(b.ToString("x2"));
+ }
+ return sb.ToString()[..8]; // short, sufficient for filenames
+ }
+ catch
+ {
+ return "default";
+ }
+ }
}
}
\ No newline at end of file
diff --git a/UnityMcpBridge/Editor/Helpers/Response.cs.meta b/UnityMcpBridge/Editor/Helpers/Response.cs.meta
index da59306..6fd11e3 100644
--- a/UnityMcpBridge/Editor/Helpers/Response.cs.meta
+++ b/UnityMcpBridge/Editor/Helpers/Response.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: 80c09a76b944f8c4691e06c4d76c4be8
\ No newline at end of file
+guid: 80c09a76b944f8c4691e06c4d76c4be8
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs.meta b/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs.meta
index 67bd7f4..dfd9023 100644
--- a/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs.meta
+++ b/UnityMcpBridge/Editor/Helpers/ServerInstaller.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: 5862c6a6d0a914f4d83224f8d039cf7b
\ No newline at end of file
+guid: 5862c6a6d0a914f4d83224f8d039cf7b
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Editor/Helpers/Vector3Helper.cs.meta b/UnityMcpBridge/Editor/Helpers/Vector3Helper.cs.meta
index 12fdb17..280381c 100644
--- a/UnityMcpBridge/Editor/Helpers/Vector3Helper.cs.meta
+++ b/UnityMcpBridge/Editor/Helpers/Vector3Helper.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: f8514fd42f23cb641a36e52550825b35
\ No newline at end of file
+guid: f8514fd42f23cb641a36e52550825b35
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Editor/Models/Command.cs.meta b/UnityMcpBridge/Editor/Models/Command.cs.meta
index 007b085..63618f5 100644
--- a/UnityMcpBridge/Editor/Models/Command.cs.meta
+++ b/UnityMcpBridge/Editor/Models/Command.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: 6754c84e5deb74749bc3a19e0c9aa280
\ No newline at end of file
+guid: 6754c84e5deb74749bc3a19e0c9aa280
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Editor/Models/MCPConfigServer.cs.meta b/UnityMcpBridge/Editor/Models/MCPConfigServer.cs.meta
index 4dad0b4..0574c5a 100644
--- a/UnityMcpBridge/Editor/Models/MCPConfigServer.cs.meta
+++ b/UnityMcpBridge/Editor/Models/MCPConfigServer.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: 5fae9d995f514e9498e9613e2cdbeca9
\ No newline at end of file
+guid: 5fae9d995f514e9498e9613e2cdbeca9
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Editor/Models/MCPConfigServers.cs.meta b/UnityMcpBridge/Editor/Models/MCPConfigServers.cs.meta
index 9ef1310..1fb5f0b 100644
--- a/UnityMcpBridge/Editor/Models/MCPConfigServers.cs.meta
+++ b/UnityMcpBridge/Editor/Models/MCPConfigServers.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: bcb583553e8173b49be71a5c43bd9502
\ No newline at end of file
+guid: bcb583553e8173b49be71a5c43bd9502
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Editor/Models/McpClient.cs.meta b/UnityMcpBridge/Editor/Models/McpClient.cs.meta
index a11df35..b08dcf3 100644
--- a/UnityMcpBridge/Editor/Models/McpClient.cs.meta
+++ b/UnityMcpBridge/Editor/Models/McpClient.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: b1afa56984aec0d41808edcebf805e6a
\ No newline at end of file
+guid: b1afa56984aec0d41808edcebf805e6a
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Editor/Models/McpConfig.cs.meta b/UnityMcpBridge/Editor/Models/McpConfig.cs.meta
index 1f70925..2a407c3 100644
--- a/UnityMcpBridge/Editor/Models/McpConfig.cs.meta
+++ b/UnityMcpBridge/Editor/Models/McpConfig.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: c17c09908f0c1524daa8b6957ce1f7f5
\ No newline at end of file
+guid: c17c09908f0c1524daa8b6957ce1f7f5
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Editor/Models/McpStatus.cs.meta b/UnityMcpBridge/Editor/Models/McpStatus.cs.meta
index 4e5feb5..e8e930d 100644
--- a/UnityMcpBridge/Editor/Models/McpStatus.cs.meta
+++ b/UnityMcpBridge/Editor/Models/McpStatus.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: aa63057c9e5282d4887352578bf49971
\ No newline at end of file
+guid: aa63057c9e5282d4887352578bf49971
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Editor/Models/McpTypes.cs.meta b/UnityMcpBridge/Editor/Models/McpTypes.cs.meta
index d20128c..377a6d0 100644
--- a/UnityMcpBridge/Editor/Models/McpTypes.cs.meta
+++ b/UnityMcpBridge/Editor/Models/McpTypes.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: 9ca97c5ff5ed74c4fbb65cfa9d2bfed1
\ No newline at end of file
+guid: 9ca97c5ff5ed74c4fbb65cfa9d2bfed1
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Editor/Models/ServerConfig.cs.meta b/UnityMcpBridge/Editor/Models/ServerConfig.cs.meta
index 0c4b377..6e675e9 100644
--- a/UnityMcpBridge/Editor/Models/ServerConfig.cs.meta
+++ b/UnityMcpBridge/Editor/Models/ServerConfig.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: e4e45386fcc282249907c2e3c7e5d9c6
\ No newline at end of file
+guid: e4e45386fcc282249907c2e3c7e5d9c6
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Editor/Tools/CommandRegistry.cs.meta b/UnityMcpBridge/Editor/Tools/CommandRegistry.cs.meta
index 55b6829..15ec884 100644
--- a/UnityMcpBridge/Editor/Tools/CommandRegistry.cs.meta
+++ b/UnityMcpBridge/Editor/Tools/CommandRegistry.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: 5b61b5a84813b5749a5c64422694a0fa
\ No newline at end of file
+guid: 5b61b5a84813b5749a5c64422694a0fa
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Editor/Tools/ExecuteMenuItem.cs.meta b/UnityMcpBridge/Editor/Tools/ExecuteMenuItem.cs.meta
index b398ddf..d9520d9 100644
--- a/UnityMcpBridge/Editor/Tools/ExecuteMenuItem.cs.meta
+++ b/UnityMcpBridge/Editor/Tools/ExecuteMenuItem.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: 896e8045986eb0d449ee68395479f1d6
\ No newline at end of file
+guid: 896e8045986eb0d449ee68395479f1d6
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Editor/Tools/ManageAsset.cs.meta b/UnityMcpBridge/Editor/Tools/ManageAsset.cs.meta
index c4d71d4..3dbc2e2 100644
--- a/UnityMcpBridge/Editor/Tools/ManageAsset.cs.meta
+++ b/UnityMcpBridge/Editor/Tools/ManageAsset.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: de90a1d9743a2874cb235cf0b83444b1
\ No newline at end of file
+guid: de90a1d9743a2874cb235cf0b83444b1
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Editor/Tools/ManageEditor.cs.meta b/UnityMcpBridge/Editor/Tools/ManageEditor.cs.meta
index ed7502e..8b55fb8 100644
--- a/UnityMcpBridge/Editor/Tools/ManageEditor.cs.meta
+++ b/UnityMcpBridge/Editor/Tools/ManageEditor.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: 43ac60aa36b361b4dbe4a038ae9f35c8
\ No newline at end of file
+guid: 43ac60aa36b361b4dbe4a038ae9f35c8
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs.meta b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs.meta
index ec958a9..5093c86 100644
--- a/UnityMcpBridge/Editor/Tools/ManageGameObject.cs.meta
+++ b/UnityMcpBridge/Editor/Tools/ManageGameObject.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: 7641d7388f0f6634b9d83d34de87b2ee
\ No newline at end of file
+guid: 7641d7388f0f6634b9d83d34de87b2ee
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Editor/Tools/ManageScene.cs.meta b/UnityMcpBridge/Editor/Tools/ManageScene.cs.meta
index 9fd63b3..532618a 100644
--- a/UnityMcpBridge/Editor/Tools/ManageScene.cs.meta
+++ b/UnityMcpBridge/Editor/Tools/ManageScene.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: b6ddda47f4077e74fbb5092388cefcc2
\ No newline at end of file
+guid: b6ddda47f4077e74fbb5092388cefcc2
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Editor/Tools/ManageScript.cs.meta b/UnityMcpBridge/Editor/Tools/ManageScript.cs.meta
index 171abb6..091cfe1 100644
--- a/UnityMcpBridge/Editor/Tools/ManageScript.cs.meta
+++ b/UnityMcpBridge/Editor/Tools/ManageScript.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: 626d2d44668019a45ae52e9ee066b7ec
\ No newline at end of file
+guid: 626d2d44668019a45ae52e9ee066b7ec
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Editor/Tools/ReadConsole.cs.meta b/UnityMcpBridge/Editor/Tools/ReadConsole.cs.meta
index 98ef717..039895f 100644
--- a/UnityMcpBridge/Editor/Tools/ReadConsole.cs.meta
+++ b/UnityMcpBridge/Editor/Tools/ReadConsole.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: 46c4f3614ed61f547ba823f0b2790267
\ No newline at end of file
+guid: 46c4f3614ed61f547ba823f0b2790267
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Editor/UnityMcpBridge.cs b/UnityMcpBridge/Editor/UnityMcpBridge.cs
index 4f3a608..760a608 100644
--- a/UnityMcpBridge/Editor/UnityMcpBridge.cs
+++ b/UnityMcpBridge/Editor/UnityMcpBridge.cs
@@ -21,6 +21,8 @@ namespace UnityMcpBridge.Editor
private static TcpListener listener;
private static bool isRunning = false;
private static readonly object lockObj = new();
+ private static readonly object startStopLock = new();
+ private static bool initScheduled = false;
private static Dictionary<
string,
(string commandJson, TaskCompletionSource tcs)
@@ -81,75 +83,148 @@ namespace UnityMcpBridge.Editor
static UnityMcpBridge()
{
- Start();
+ // Use delayed initialization to avoid repeated restarts during compilation
+ EditorApplication.delayCall += InitializeAfterCompilation;
EditorApplication.quitting += Stop;
+ AssemblyReloadEvents.beforeAssemblyReload += Stop; // ensure listener releases before domain reload
+
+ // Robust re-init hooks
+ UnityEditor.Compilation.CompilationPipeline.compilationFinished += _ => ScheduleInitRetry();
+ EditorApplication.playModeStateChanged += state =>
+ {
+ if (state == PlayModeStateChange.EnteredEditMode || state == PlayModeStateChange.EnteredPlayMode)
+ {
+ ScheduleInitRetry();
+ }
+ };
+ }
+
+ ///
+ /// Initialize the MCP bridge after Unity is fully loaded and compilation is complete.
+ /// This prevents repeated restarts during script compilation that cause port hopping.
+ ///
+ private static void InitializeAfterCompilation()
+ {
+ initScheduled = false;
+
+ // Play-mode friendly: allow starting in play mode; only defer while compiling
+ if (EditorApplication.isCompiling)
+ {
+ ScheduleInitRetry();
+ return;
+ }
+
+ if (!isRunning)
+ {
+ Start();
+ if (!isRunning)
+ {
+ // If a race prevented start, retry later
+ ScheduleInitRetry();
+ }
+ }
+ }
+
+ private static void ScheduleInitRetry()
+ {
+ if (initScheduled)
+ {
+ return;
+ }
+ initScheduled = true;
+ EditorApplication.delayCall += InitializeAfterCompilation;
}
public static void Start()
{
- Stop();
-
- try
+ lock (startStopLock)
{
- ServerInstaller.EnsureServerInstalled();
- }
- catch (Exception ex)
- {
- Debug.LogError($"Failed to ensure UnityMcpServer is installed: {ex.Message}");
- }
-
- if (isRunning)
- {
- return;
- }
-
- try
- {
- // Use PortManager to get available port with automatic fallback
- currentUnityPort = PortManager.GetPortWithFallback();
-
- listener = new TcpListener(IPAddress.Loopback, currentUnityPort);
- listener.Start();
- isRunning = true;
- isAutoConnectMode = false; // Normal startup mode
- Debug.Log($"UnityMcpBridge started on port {currentUnityPort}.");
- // Assuming ListenerLoop and ProcessCommands are defined elsewhere
- Task.Run(ListenerLoop);
- EditorApplication.update += ProcessCommands;
- }
- catch (SocketException ex)
- {
- if (ex.SocketErrorCode == SocketError.AddressAlreadyInUse)
+ // Don't restart if already running on a working port
+ if (isRunning && listener != null)
{
- Debug.LogError(
- $"Port {currentUnityPort} is already in use. This should not happen with dynamic port allocation."
- );
+ Debug.Log($"UnityMcpBridge already running on port {currentUnityPort}");
+ return;
}
- else
+
+ Stop();
+
+ // Removed automatic server installer; assume server exists inside the package (UPM).
+
+ try
{
- Debug.LogError($"Failed to start TCP listener: {ex.Message}");
+ // Try to reuse the current port if it's still available, otherwise get a new one
+ if (currentUnityPort > 0 && PortManager.IsPortAvailable(currentUnityPort))
+ {
+ Debug.Log($"Reusing current port {currentUnityPort}");
+ }
+ else
+ {
+ // Use PortManager to get available port with automatic fallback
+ currentUnityPort = PortManager.GetPortWithFallback();
+ }
+
+ listener = new TcpListener(IPAddress.Loopback, currentUnityPort);
+ listener.Start();
+ isRunning = true;
+ isAutoConnectMode = false; // Normal startup mode
+ Debug.Log($"UnityMcpBridge started on port {currentUnityPort}.");
+ // Assuming ListenerLoop and ProcessCommands are defined elsewhere
+ Task.Run(ListenerLoop);
+ EditorApplication.update += ProcessCommands;
+ }
+ catch (SocketException ex)
+ {
+ if (ex.SocketErrorCode == SocketError.AddressAlreadyInUse)
+ {
+ Debug.LogError(
+ $"Port {currentUnityPort} is already in use. Trying to find alternative..."
+ );
+
+ // Try once more with a fresh port discovery
+ try
+ {
+ currentUnityPort = PortManager.DiscoverNewPort();
+ listener = new TcpListener(IPAddress.Loopback, currentUnityPort);
+ listener.Start();
+ isRunning = true;
+ Debug.Log($"UnityMcpBridge started on fallback port {currentUnityPort}.");
+ Task.Run(ListenerLoop);
+ EditorApplication.update += ProcessCommands;
+ }
+ catch (Exception fallbackEx)
+ {
+ Debug.LogError($"Failed to start on fallback port: {fallbackEx.Message}");
+ }
+ }
+ else
+ {
+ Debug.LogError($"Failed to start TCP listener: {ex.Message}");
+ }
}
}
}
public static void Stop()
{
- if (!isRunning)
+ lock (startStopLock)
{
- return;
- }
+ if (!isRunning)
+ {
+ return;
+ }
- try
- {
- listener?.Stop();
- listener = null;
- isRunning = false;
- EditorApplication.update -= ProcessCommands;
- Debug.Log("UnityMcpBridge stopped.");
- }
- catch (Exception ex)
- {
- Debug.LogError($"Error stopping UnityMcpBridge: {ex.Message}");
+ try
+ {
+ listener?.Stop();
+ listener = null;
+ isRunning = false;
+ EditorApplication.update -= ProcessCommands;
+ Debug.Log("UnityMcpBridge stopped.");
+ }
+ catch (Exception ex)
+ {
+ Debug.LogError($"Error stopping UnityMcpBridge: {ex.Message}");
+ }
}
}
diff --git a/UnityMcpBridge/Editor/UnityMcpBridge.cs.meta b/UnityMcpBridge/Editor/UnityMcpBridge.cs.meta
index 3915698..dcaa761 100644
--- a/UnityMcpBridge/Editor/UnityMcpBridge.cs.meta
+++ b/UnityMcpBridge/Editor/UnityMcpBridge.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: 1e0fb0e418dd19345a8236c44078972b
\ No newline at end of file
+guid: 1e0fb0e418dd19345a8236c44078972b
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Editor/Windows/ManualConfigEditorWindow.cs.meta b/UnityMcpBridge/Editor/Windows/ManualConfigEditorWindow.cs.meta
index b5797cc..41646e6 100644
--- a/UnityMcpBridge/Editor/Windows/ManualConfigEditorWindow.cs.meta
+++ b/UnityMcpBridge/Editor/Windows/ManualConfigEditorWindow.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: 36798bd7b867b8e43ac86885e94f928f
\ No newline at end of file
+guid: 36798bd7b867b8e43ac86885e94f928f
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Editor/Windows/UnityMCPEditorWindow.cs.meta b/UnityMcpBridge/Editor/Windows/UnityMCPEditorWindow.cs.meta
index 0229c75..c492a9d 100644
--- a/UnityMcpBridge/Editor/Windows/UnityMCPEditorWindow.cs.meta
+++ b/UnityMcpBridge/Editor/Windows/UnityMCPEditorWindow.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: 4283e255b343c4546b843cd22214ac93
\ No newline at end of file
+guid: 4283e255b343c4546b843cd22214ac93
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs
index 14c9f5b..691ef45 100644
--- a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs
+++ b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs
@@ -58,6 +58,8 @@ namespace UnityMcpBridge.Editor.Windows
private void OnFocus()
{
+ // Refresh bridge running state on focus in case initialization completed after domain reload
+ isUnityBridgeRunning = UnityMcpBridge.IsRunning;
if (mcpClients.clients.Count > 0 && selectedClientIndex < mcpClients.clients.Count)
{
McpClient selectedClient = mcpClients.clients[selectedClientIndex];
@@ -255,6 +257,9 @@ namespace UnityMcpBridge.Editor.Windows
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
+ // Always reflect the live state each repaint to avoid stale UI after recompiles
+ isUnityBridgeRunning = UnityMcpBridge.IsRunning;
+
GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel)
{
fontSize = 14
@@ -458,8 +463,9 @@ namespace UnityMcpBridge.Editor.Windows
{
UnityMcpBridge.Start();
}
-
- isUnityBridgeRunning = !isUnityBridgeRunning;
+ // Reflect the actual state post-operation (avoid optimistic toggle)
+ isUnityBridgeRunning = UnityMcpBridge.IsRunning;
+ Repaint();
}
private string WriteToConfig(string pythonDir, string configPath, McpClient mcpClient = null)
diff --git a/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs.meta b/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs.meta
index 437ccab..fb13126 100644
--- a/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs.meta
+++ b/UnityMcpBridge/Editor/Windows/VSCodeManualSetupWindow.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: 377fe73d52cf0435fabead5f50a0d204
\ No newline at end of file
+guid: 377fe73d52cf0435fabead5f50a0d204
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpBridge/Runtime/Serialization/UnityTypeConverters.cs.meta b/UnityMcpBridge/Runtime/Serialization/UnityTypeConverters.cs.meta
index 9596160..caaf285 100644
--- a/UnityMcpBridge/Runtime/Serialization/UnityTypeConverters.cs.meta
+++ b/UnityMcpBridge/Runtime/Serialization/UnityTypeConverters.cs.meta
@@ -1,2 +1,11 @@
fileFormatVersion: 2
-guid: e65311c160f0d41d4a1b45a3dba8dd5a
\ No newline at end of file
+guid: e65311c160f0d41d4a1b45a3dba8dd5a
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/UnityMcpServer/src/port_discovery.py b/UnityMcpServer/src/port_discovery.py
index a0dfe96..c09efe3 100644
--- a/UnityMcpServer/src/port_discovery.py
+++ b/UnityMcpServer/src/port_discovery.py
@@ -1,69 +1,133 @@
"""
Port discovery utility for Unity MCP Server.
-Reads port configuration saved by Unity Bridge.
+
+What changed and why:
+- Unity now writes a per-project port file named like
+ `~/.unity-mcp/unity-mcp-port-.json` to avoid projects overwriting
+ each other's saved port. The legacy file `unity-mcp-port.json` may still
+ exist.
+- This module now scans for both patterns, prefers the most recently
+ modified file, and verifies that the port is actually a Unity MCP listener
+ (quick socket connect + ping) before choosing it.
"""
import json
import os
import logging
from pathlib import Path
-from typing import Optional
+from typing import Optional, List
+import glob
+import socket
logger = logging.getLogger("unity-mcp-server")
class PortDiscovery:
"""Handles port discovery from Unity Bridge registry"""
-
- REGISTRY_FILE = "unity-mcp-port.json"
+ REGISTRY_FILE = "unity-mcp-port.json" # legacy single-project file
+ DEFAULT_PORT = 6400
+ CONNECT_TIMEOUT = 0.3 # seconds, keep this snappy during discovery
@staticmethod
def get_registry_path() -> Path:
"""Get the path to the port registry file"""
return Path.home() / ".unity-mcp" / PortDiscovery.REGISTRY_FILE
+ @staticmethod
+ def get_registry_dir() -> Path:
+ return Path.home() / ".unity-mcp"
+
+ @staticmethod
+ def list_candidate_files() -> List[Path]:
+ """Return candidate registry files, newest first.
+ Includes hashed per-project files and the legacy file (if present).
+ """
+ base = PortDiscovery.get_registry_dir()
+ hashed = sorted(
+ (Path(p) for p in glob.glob(str(base / "unity-mcp-port-*.json"))),
+ key=lambda p: p.stat().st_mtime,
+ reverse=True,
+ )
+ legacy = PortDiscovery.get_registry_path()
+ if legacy.exists():
+ # Put legacy at the end so hashed, per-project files win
+ hashed.append(legacy)
+ return hashed
+
+ @staticmethod
+ def _try_probe_unity_mcp(port: int) -> bool:
+ """Quickly check if a Unity MCP listener is on this port.
+ Tries a short TCP connect, sends 'ping', expects a JSON 'pong'.
+ """
+ try:
+ with socket.create_connection(("127.0.0.1", port), PortDiscovery.CONNECT_TIMEOUT) as s:
+ s.settimeout(PortDiscovery.CONNECT_TIMEOUT)
+ try:
+ s.sendall(b"ping")
+ data = s.recv(512)
+ # Minimal validation: look for a success pong response
+ if data and b'"message":"pong"' in data:
+ return True
+ except Exception:
+ # Even if the ping fails, a successful TCP connect is a strong signal.
+ # Fall back to treating the port as viable if connect succeeded.
+ return True
+ except Exception:
+ return False
+ return False
+
@staticmethod
def discover_unity_port() -> int:
"""
- Discover Unity port from registry file with fallback to default
+ Discover Unity port by scanning per-project and legacy registry files.
+ Prefer the newest file whose port responds; fall back to first parsed
+ value; finally default to 6400.
Returns:
Port number to connect to
"""
- registry_file = PortDiscovery.get_registry_path()
-
- if registry_file.exists():
+ candidates = PortDiscovery.list_candidate_files()
+
+ first_seen_port: Optional[int] = None
+
+ for path in candidates:
try:
- with open(registry_file, 'r') as f:
- port_config = json.load(f)
-
- unity_port = port_config.get('unity_port')
- if unity_port and isinstance(unity_port, int):
- logger.info(f"Discovered Unity port from registry: {unity_port}")
- return unity_port
-
+ with open(path, 'r') as f:
+ cfg = json.load(f)
+ unity_port = cfg.get('unity_port')
+ if isinstance(unity_port, int):
+ if first_seen_port is None:
+ first_seen_port = unity_port
+ if PortDiscovery._try_probe_unity_mcp(unity_port):
+ logger.info(f"Using Unity port from {path.name}: {unity_port}")
+ return unity_port
except Exception as e:
- logger.warning(f"Could not read port registry: {e}")
-
+ logger.warning(f"Could not read port registry {path}: {e}")
+
+ if first_seen_port is not None:
+ logger.info(f"No responsive port found; using first seen value {first_seen_port}")
+ return first_seen_port
+
# Fallback to default port
- logger.info("No port registry found, using default port 6400")
- return 6400
+ logger.info(f"No port registry found; using default port {PortDiscovery.DEFAULT_PORT}")
+ return PortDiscovery.DEFAULT_PORT
@staticmethod
def get_port_config() -> Optional[dict]:
"""
- Get the full port configuration from registry
+ Get the most relevant port configuration from registry.
+ Returns the most recent hashed file's config if present,
+ otherwise the legacy file's config. Returns None if nothing exists.
Returns:
Port configuration dict or None if not found
"""
- registry_file = PortDiscovery.get_registry_path()
-
- if not registry_file.exists():
+ candidates = PortDiscovery.list_candidate_files()
+ if not candidates:
return None
-
- try:
- with open(registry_file, 'r') as f:
- return json.load(f)
- except Exception as e:
- logger.warning(f"Could not read port configuration: {e}")
- return None
\ No newline at end of file
+ for path in candidates:
+ try:
+ with open(path, 'r') as f:
+ return json.load(f)
+ except Exception as e:
+ logger.warning(f"Could not read port configuration {path}: {e}")
+ return None
\ No newline at end of file