using System; using System.IO; using System.Net; using System.Net.Sockets; using System.Security.Cryptography; using System.Text; using System.Threading; using MCPForUnity.Editor.Constants; using Newtonsoft.Json; using UnityEditor; using UnityEngine; namespace MCPForUnity.Editor.Helpers { /// /// Manages dynamic port allocation and persistent storage for MCP for Unity /// public static class PortManager { private static bool IsDebugEnabled() { try { return EditorPrefs.GetBool(EditorPrefKeys.DebugLogs, false); } catch { return false; } } 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; } /// /// Get the port to use from storage, or return the default if none has been saved yet. /// /// Port number to use public static int GetPortWithFallback() { var storedConfig = GetStoredPortConfig(); if (storedConfig != null && storedConfig.unity_port > 0 && string.Equals(storedConfig.project_path ?? string.Empty, Application.dataPath ?? string.Empty, StringComparison.OrdinalIgnoreCase)) { return storedConfig.unity_port; } return DefaultPort; } /// /// Discover and save a new available port (used by Auto-Connect button) /// /// New available port public static int DiscoverNewPort() { int newPort = FindAvailablePort(); SavePort(newPort); if (IsDebugEnabled()) McpLog.Info($"Discovered and saved new port: {newPort}"); return newPort; } /// /// Persist a user-selected port and return the value actually stored. /// If is unavailable, the next available port is chosen instead. /// public static int SetPreferredPort(int port) { if (port <= 0) { throw new ArgumentOutOfRangeException(nameof(port), "Port must be positive."); } if (!IsPortAvailable(port)) { throw new InvalidOperationException($"Port {port} is already in use."); } SavePort(port); return port; } /// /// Find an available port starting from the default port /// /// Available port number private static int FindAvailablePort() { // Always try default port first if (IsPortAvailable(DefaultPort)) { if (IsDebugEnabled()) McpLog.Info($"Using default port {DefaultPort}"); return DefaultPort; } if (IsDebugEnabled()) McpLog.Info($"Default port {DefaultPort} is in use, searching for alternative..."); // Search for alternatives for (int port = DefaultPort + 1; port < DefaultPort + MaxPortAttempts; port++) { if (IsPortAvailable(port)) { if (IsDebugEnabled()) McpLog.Info($"Found available port {port}"); return port; } } throw new Exception($"No available ports found in range {DefaultPort}-{DefaultPort + MaxPortAttempts}"); } /// /// Check if a specific port is available for binding /// /// Port to check /// True if port is available public static bool IsPortAvailable(int port) { try { var testListener = new TcpListener(IPAddress.Loopback, port); testListener.Start(); testListener.Stop(); return true; } catch (SocketException) { return false; } } /// /// Check if a port is currently being used by MCP for Unity /// This helps avoid unnecessary port changes when Unity itself is using the port /// /// Port to check /// True if port appears to be used by MCP for Unity public static bool IsPortUsedByMCPForUnity(int port) { try { // Try to make a quick connection to see if it's an MCP for Unity 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 MCP for Unity 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 (!IsPortUsedByMCPForUnity(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 /// /// Port to save 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); string registryFile = GetRegistryFilePath(); string json = JsonConvert.SerializeObject(portConfig, Formatting.Indented); // Write to hashed, project-scoped file File.WriteAllText(registryFile, json, new System.Text.UTF8Encoding(false)); // Also write to legacy stable filename to avoid hash/case drift across reloads string legacy = Path.Combine(GetRegistryDirectory(), RegistryFileName); File.WriteAllText(legacy, json, new System.Text.UTF8Encoding(false)); if (IsDebugEnabled()) McpLog.Info($"Saved port {port} to storage"); } catch (Exception ex) { McpLog.Warn($"Could not save port to storage: {ex.Message}"); } } /// /// Load port from persistent storage /// /// Stored port number, or 0 if not found private static int LoadStoredPort() { try { string registryFile = GetRegistryFilePath(); if (!File.Exists(registryFile)) { // 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); var portConfig = JsonConvert.DeserializeObject(json); return portConfig?.unity_port ?? 0; } catch (Exception ex) { McpLog.Warn($"Could not load port from storage: {ex.Message}"); return 0; } } /// /// Get the current stored port configuration /// /// Port configuration if exists, null otherwise public static PortConfig GetStoredPortConfig() { try { string registryFile = GetRegistryFilePath(); if (!File.Exists(registryFile)) { // 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); return JsonConvert.DeserializeObject(json); } catch (Exception ex) { McpLog.Warn($"Could not load port config: {ex.Message}"); return null; } } private static string GetRegistryDirectory() { 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"; } } } }