2025-07-29 12:17:36 +08:00
using System ;
using System.IO ;
2025-08-10 03:05:47 +08:00
using UnityEditor ;
2025-07-29 12:17:36 +08:00
using System.Net ;
using System.Net.Sockets ;
2025-08-08 06:32:03 +08:00
using System.Security.Cryptography ;
using System.Text ;
using System.Threading ;
2025-07-29 12:17:36 +08:00
using Newtonsoft.Json ;
using UnityEngine ;
2025-08-21 03:59:49 +08:00
namespace MCPForUnity.Editor.Helpers
2025-07-29 12:17:36 +08:00
{
/// <summary>
2025-08-21 03:59:49 +08:00
/// Manages dynamic port allocation and persistent storage for MCP for Unity
2025-07-29 12:17:36 +08:00
/// </summary>
public static class PortManager
{
2025-08-10 03:05:47 +08:00
private static bool IsDebugEnabled ( )
{
2025-08-21 03:59:49 +08:00
try { return EditorPrefs . GetBool ( "MCPForUnity.DebugLogs" , false ) ; }
2025-08-10 03:05:47 +08:00
catch { return false ; }
}
2025-07-29 12:17:36 +08:00
private const int DefaultPort = 6400 ;
private const int MaxPortAttempts = 100 ;
private const string RegistryFileName = "unity-mcp-port.json" ;
[Serializable]
public class PortConfig
{
public int unity_port ;
public string created_date ;
public string project_path ;
}
/// <summary>
/// Get the port to use - either from storage or discover a new one
/// Will try stored port first, then fallback to discovering new port
/// </summary>
/// <returns>Port number to use</returns>
public static int GetPortWithFallback ( )
{
2025-08-08 06:32:03 +08:00
// 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 & &
2025-08-08 23:32:20 +08:00
string . Equals ( storedConfig . project_path ? ? string . Empty , Application . dataPath ? ? string . Empty , StringComparison . OrdinalIgnoreCase ) & &
2025-08-08 06:32:03 +08:00
IsPortAvailable ( storedConfig . unity_port ) )
2025-07-29 12:17:36 +08:00
{
2025-08-21 03:59:49 +08:00
if ( IsDebugEnabled ( ) ) Debug . Log ( $"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Using stored port {storedConfig.unity_port} for current project" ) ;
2025-08-08 06:32:03 +08:00
return storedConfig . unity_port ;
2025-07-29 12:17:36 +08:00
}
2025-08-08 06:32:03 +08:00
// 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 ) )
{
2025-08-21 03:59:49 +08:00
if ( IsDebugEnabled ( ) ) Debug . Log ( $"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Stored port {storedConfig.unity_port} became available after short wait" ) ;
2025-08-08 06:32:03 +08:00
return storedConfig . unity_port ;
}
2025-08-08 23:37:02 +08:00
// Prefer sticking to the same port; let the caller handle bind retries/fallbacks
return storedConfig . unity_port ;
2025-08-08 06:32:03 +08:00
}
// If no valid stored port, find a new one and save it
2025-07-29 12:17:36 +08:00
int newPort = FindAvailablePort ( ) ;
SavePort ( newPort ) ;
return newPort ;
}
/// <summary>
/// Discover and save a new available port (used by Auto-Connect button)
/// </summary>
/// <returns>New available port</returns>
public static int DiscoverNewPort ( )
{
int newPort = FindAvailablePort ( ) ;
SavePort ( newPort ) ;
2025-08-21 03:59:49 +08:00
if ( IsDebugEnabled ( ) ) Debug . Log ( $"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Discovered and saved new port: {newPort}" ) ;
2025-07-29 12:17:36 +08:00
return newPort ;
}
/// <summary>
/// Find an available port starting from the default port
/// </summary>
/// <returns>Available port number</returns>
private static int FindAvailablePort ( )
{
// Always try default port first
if ( IsPortAvailable ( DefaultPort ) )
{
2025-08-21 03:59:49 +08:00
if ( IsDebugEnabled ( ) ) Debug . Log ( $"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Using default port {DefaultPort}" ) ;
2025-07-29 12:17:36 +08:00
return DefaultPort ;
}
2025-08-21 03:59:49 +08:00
if ( IsDebugEnabled ( ) ) Debug . Log ( $"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Default port {DefaultPort} is in use, searching for alternative..." ) ;
2025-07-29 12:17:36 +08:00
// Search for alternatives
for ( int port = DefaultPort + 1 ; port < DefaultPort + MaxPortAttempts ; port + + )
{
if ( IsPortAvailable ( port ) )
{
2025-08-21 03:59:49 +08:00
if ( IsDebugEnabled ( ) ) Debug . Log ( $"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Found available port {port}" ) ;
2025-07-29 12:17:36 +08:00
return port ;
}
}
throw new Exception ( $"No available ports found in range {DefaultPort}-{DefaultPort + MaxPortAttempts}" ) ;
}
/// <summary>
2025-08-08 06:32:03 +08:00
/// Check if a specific port is available for binding
2025-07-29 12:17:36 +08:00
/// </summary>
/// <param name="port">Port to check</param>
/// <returns>True if port is available</returns>
public static bool IsPortAvailable ( int port )
{
try
{
var testListener = new TcpListener ( IPAddress . Loopback , port ) ;
testListener . Start ( ) ;
testListener . Stop ( ) ;
return true ;
}
catch ( SocketException )
{
return false ;
}
}
2025-08-08 06:32:03 +08:00
/// <summary>
2025-08-21 03:59:49 +08:00
/// Check if a port is currently being used by MCP for Unity
2025-08-08 06:32:03 +08:00
/// This helps avoid unnecessary port changes when Unity itself is using the port
/// </summary>
/// <param name="port">Port to check</param>
2025-08-21 03:59:49 +08:00
/// <returns>True if port appears to be used by MCP for Unity</returns>
public static bool IsPortUsedByMCPForUnity ( int port )
2025-08-08 06:32:03 +08:00
{
try
{
2025-08-21 03:59:49 +08:00
// Try to make a quick connection to see if it's an MCP for Unity server
2025-08-08 06:32:03 +08:00
using var client = new TcpClient ( ) ;
var connectTask = client . ConnectAsync ( IPAddress . Loopback , port ) ;
if ( connectTask . Wait ( 100 ) ) // 100ms timeout
{
2025-08-21 03:59:49 +08:00
// If connection succeeded, it's likely the MCP for Unity server
2025-08-08 06:32:03 +08:00
return client . Connected ;
}
return false ;
}
catch
{
return false ;
}
}
/// <summary>
/// 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.
/// </summary>
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
2025-08-21 03:59:49 +08:00
if ( ! IsPortUsedByMCPForUnity ( port ) )
2025-08-08 06:32:03 +08:00
{
// In use by something else; don't keep waiting
return false ;
}
Thread . Sleep ( step ) ;
waited + = step ;
}
return IsPortAvailable ( port ) ;
}
2025-07-29 12:17:36 +08:00
/// <summary>
/// Save port to persistent storage
/// </summary>
/// <param name="port">Port to save</param>
private static void SavePort ( int port )
{
try
{
var portConfig = new PortConfig
{
unity_port = port ,
created_date = DateTime . UtcNow . ToString ( "O" ) ,
project_path = Application . dataPath
} ;
string registryDir = GetRegistryDirectory ( ) ;
Directory . CreateDirectory ( registryDir ) ;
2025-08-08 06:32:03 +08:00
string registryFile = GetRegistryFilePath ( ) ;
2025-07-29 12:17:36 +08:00
string json = JsonConvert . SerializeObject ( portConfig , Formatting . Indented ) ;
2025-08-08 23:32:20 +08:00
// Write to hashed, project-scoped file
2025-08-24 18:57:11 +08:00
File . WriteAllText ( registryFile , json , new System . Text . UTF8Encoding ( false ) ) ;
2025-08-08 23:32:20 +08:00
// Also write to legacy stable filename to avoid hash/case drift across reloads
string legacy = Path . Combine ( GetRegistryDirectory ( ) , RegistryFileName ) ;
2025-08-24 18:57:11 +08:00
File . WriteAllText ( legacy , json , new System . Text . UTF8Encoding ( false ) ) ;
2025-07-29 12:17:36 +08:00
2025-08-21 03:59:49 +08:00
if ( IsDebugEnabled ( ) ) Debug . Log ( $"<b><color=#2EA3FF>MCP-FOR-UNITY</color></b>: Saved port {port} to storage" ) ;
2025-07-29 12:17:36 +08:00
}
catch ( Exception ex )
{
Debug . LogWarning ( $"Could not save port to storage: {ex.Message}" ) ;
}
}
/// <summary>
/// Load port from persistent storage
/// </summary>
/// <returns>Stored port number, or 0 if not found</returns>
private static int LoadStoredPort ( )
{
try
{
2025-08-08 06:32:03 +08:00
string registryFile = GetRegistryFilePath ( ) ;
2025-07-29 12:17:36 +08:00
if ( ! File . Exists ( registryFile ) )
{
2025-08-08 06:32:03 +08:00
// Backwards compatibility: try the legacy file name
string legacy = Path . Combine ( GetRegistryDirectory ( ) , RegistryFileName ) ;
if ( ! File . Exists ( legacy ) )
{
return 0 ;
}
registryFile = legacy ;
2025-07-29 12:17:36 +08:00
}
string json = File . ReadAllText ( registryFile ) ;
var portConfig = JsonConvert . DeserializeObject < PortConfig > ( json ) ;
return portConfig ? . unity_port ? ? 0 ;
}
catch ( Exception ex )
{
Debug . LogWarning ( $"Could not load port from storage: {ex.Message}" ) ;
return 0 ;
}
}
/// <summary>
/// Get the current stored port configuration
/// </summary>
/// <returns>Port configuration if exists, null otherwise</returns>
public static PortConfig GetStoredPortConfig ( )
{
try
{
2025-08-08 06:32:03 +08:00
string registryFile = GetRegistryFilePath ( ) ;
2025-07-29 12:17:36 +08:00
if ( ! File . Exists ( registryFile ) )
{
2025-08-08 06:32:03 +08:00
// Backwards compatibility: try the legacy file
string legacy = Path . Combine ( GetRegistryDirectory ( ) , RegistryFileName ) ;
if ( ! File . Exists ( legacy ) )
{
return null ;
}
registryFile = legacy ;
2025-07-29 12:17:36 +08:00
}
string json = File . ReadAllText ( registryFile ) ;
return JsonConvert . DeserializeObject < PortConfig > ( json ) ;
}
catch ( Exception ex )
{
Debug . LogWarning ( $"Could not load port config: {ex.Message}" ) ;
return null ;
}
}
private static string GetRegistryDirectory ( )
{
return Path . Combine ( Environment . GetFolderPath ( Environment . SpecialFolder . UserProfile ) , ".unity-mcp" ) ;
}
2025-08-08 06:32:03 +08:00
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" ;
}
}
2025-07-29 12:17:36 +08:00
}
}