22using System . IO ;
33using System . Net ;
44using System . Net . Sockets ;
5+ using System . Security . Cryptography ;
6+ using System . Text ;
7+ using System . Threading ;
58using Newtonsoft . Json ;
69using UnityEngine ;
710
@@ -31,15 +34,28 @@ public class PortConfig
3134 /// <returns>Port number to use</returns>
3235 public static int GetPortWithFallback ( )
3336 {
34- // Try to load stored port first
35- int storedPort = LoadStoredPort ( ) ;
36- if ( storedPort > 0 && IsPortAvailable ( storedPort ) )
37+ // Try to load stored port first, but only if it's from the current project
38+ var storedConfig = GetStoredPortConfig ( ) ;
39+ if ( storedConfig != null &&
40+ storedConfig . unity_port > 0 &&
41+ storedConfig . project_path == Application . dataPath &&
42+ IsPortAvailable ( storedConfig . unity_port ) )
3743 {
38- Debug . Log ( $ "Using stored port { storedPort } ") ;
39- return storedPort ;
44+ Debug . Log ( $ "Using stored port { storedConfig . unity_port } for current project ") ;
45+ return storedConfig . unity_port ;
4046 }
4147
42- // If no stored port or stored port is unavailable, find a new one
48+ // If stored port exists but is currently busy, wait briefly for release
49+ if ( storedConfig != null && storedConfig . unity_port > 0 )
50+ {
51+ if ( WaitForPortRelease ( storedConfig . unity_port , 1500 ) )
52+ {
53+ Debug . Log ( $ "Stored port { storedConfig . unity_port } became available after short wait") ;
54+ return storedConfig . unity_port ;
55+ }
56+ }
57+
58+ // If no valid stored port, find a new one and save it
4359 int newPort = FindAvailablePort ( ) ;
4460 SavePort ( newPort ) ;
4561 return newPort ;
@@ -86,7 +102,7 @@ private static int FindAvailablePort()
86102 }
87103
88104 /// <summary>
89- /// Check if a specific port is available
105+ /// Check if a specific port is available for binding
90106 /// </summary>
91107 /// <param name="port">Port to check</param>
92108 /// <returns>True if port is available</returns>
@@ -105,6 +121,61 @@ public static bool IsPortAvailable(int port)
105121 }
106122 }
107123
124+ /// <summary>
125+ /// Check if a port is currently being used by Unity MCP Bridge
126+ /// This helps avoid unnecessary port changes when Unity itself is using the port
127+ /// </summary>
128+ /// <param name="port">Port to check</param>
129+ /// <returns>True if port appears to be used by Unity MCP</returns>
130+ public static bool IsPortUsedByUnityMcp ( int port )
131+ {
132+ try
133+ {
134+ // Try to make a quick connection to see if it's a Unity MCP server
135+ using var client = new TcpClient ( ) ;
136+ var connectTask = client . ConnectAsync ( IPAddress . Loopback , port ) ;
137+ if ( connectTask . Wait ( 100 ) ) // 100ms timeout
138+ {
139+ // If connection succeeded, it's likely the Unity MCP server
140+ return client . Connected ;
141+ }
142+ return false ;
143+ }
144+ catch
145+ {
146+ return false ;
147+ }
148+ }
149+
150+ /// <summary>
151+ /// Wait for a port to become available for a limited amount of time.
152+ /// Used to bridge the gap during domain reload when the old listener
153+ /// hasn't released the socket yet.
154+ /// </summary>
155+ private static bool WaitForPortRelease ( int port , int timeoutMs )
156+ {
157+ int waited = 0 ;
158+ const int step = 100 ;
159+ while ( waited < timeoutMs )
160+ {
161+ if ( IsPortAvailable ( port ) )
162+ {
163+ return true ;
164+ }
165+
166+ // If the port is in use by an MCP instance, continue waiting briefly
167+ if ( ! IsPortUsedByUnityMcp ( port ) )
168+ {
169+ // In use by something else; don't keep waiting
170+ return false ;
171+ }
172+
173+ Thread . Sleep ( step ) ;
174+ waited += step ;
175+ }
176+ return IsPortAvailable ( port ) ;
177+ }
178+
108179 /// <summary>
109180 /// Save port to persistent storage
110181 /// </summary>
@@ -123,7 +194,7 @@ private static void SavePort(int port)
123194 string registryDir = GetRegistryDirectory ( ) ;
124195 Directory . CreateDirectory ( registryDir ) ;
125196
126- string registryFile = Path . Combine ( registryDir , RegistryFileName ) ;
197+ string registryFile = GetRegistryFilePath ( ) ;
127198 string json = JsonConvert . SerializeObject ( portConfig , Formatting . Indented ) ;
128199 File . WriteAllText ( registryFile , json ) ;
129200
@@ -143,11 +214,17 @@ private static int LoadStoredPort()
143214 {
144215 try
145216 {
146- string registryFile = Path . Combine ( GetRegistryDirectory ( ) , RegistryFileName ) ;
217+ string registryFile = GetRegistryFilePath ( ) ;
147218
148219 if ( ! File . Exists ( registryFile ) )
149220 {
150- return 0 ;
221+ // Backwards compatibility: try the legacy file name
222+ string legacy = Path . Combine ( GetRegistryDirectory ( ) , RegistryFileName ) ;
223+ if ( ! File . Exists ( legacy ) )
224+ {
225+ return 0 ;
226+ }
227+ registryFile = legacy ;
151228 }
152229
153230 string json = File . ReadAllText ( registryFile ) ;
@@ -170,11 +247,17 @@ public static PortConfig GetStoredPortConfig()
170247 {
171248 try
172249 {
173- string registryFile = Path . Combine ( GetRegistryDirectory ( ) , RegistryFileName ) ;
250+ string registryFile = GetRegistryFilePath ( ) ;
174251
175252 if ( ! File . Exists ( registryFile ) )
176253 {
177- return null ;
254+ // Backwards compatibility: try the legacy file
255+ string legacy = Path . Combine ( GetRegistryDirectory ( ) , RegistryFileName ) ;
256+ if ( ! File . Exists ( legacy ) )
257+ {
258+ return null ;
259+ }
260+ registryFile = legacy ;
178261 }
179262
180263 string json = File . ReadAllText ( registryFile ) ;
@@ -191,5 +274,33 @@ private static string GetRegistryDirectory()
191274 {
192275 return Path . Combine ( Environment . GetFolderPath ( Environment . SpecialFolder . UserProfile ) , ".unity-mcp" ) ;
193276 }
277+
278+ private static string GetRegistryFilePath ( )
279+ {
280+ string dir = GetRegistryDirectory ( ) ;
281+ string hash = ComputeProjectHash ( Application . dataPath ) ;
282+ string fileName = $ "unity-mcp-port-{ hash } .json";
283+ return Path . Combine ( dir , fileName ) ;
284+ }
285+
286+ private static string ComputeProjectHash ( string input )
287+ {
288+ try
289+ {
290+ using SHA1 sha1 = SHA1 . Create ( ) ;
291+ byte [ ] bytes = Encoding . UTF8 . GetBytes ( input ?? string . Empty ) ;
292+ byte [ ] hashBytes = sha1 . ComputeHash ( bytes ) ;
293+ var sb = new StringBuilder ( ) ;
294+ foreach ( byte b in hashBytes )
295+ {
296+ sb . Append ( b . ToString ( "x2" ) ) ;
297+ }
298+ return sb . ToString ( ) [ ..8 ] ; // short, sufficient for filenames
299+ }
300+ catch
301+ {
302+ return "default" ;
303+ }
304+ }
194305 }
195306}
0 commit comments