diff --git a/UnityMcpBridge/Editor/Helpers/PortManager.cs b/UnityMcpBridge/Editor/Helpers/PortManager.cs new file mode 100644 index 00000000..8e368a6a --- /dev/null +++ b/UnityMcpBridge/Editor/Helpers/PortManager.cs @@ -0,0 +1,195 @@ +using System; +using System.IO; +using System.Net; +using System.Net.Sockets; +using Newtonsoft.Json; +using UnityEngine; + +namespace UnityMcpBridge.Editor.Helpers +{ + /// + /// Manages dynamic port allocation and persistent storage for Unity MCP Bridge + /// + public static class PortManager + { + 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 - either from storage or discover a new one + /// Will try stored port first, then fallback to discovering new port + /// + /// Port number to use + public static int GetPortWithFallback() + { + // Try to load stored port first + int storedPort = LoadStoredPort(); + if (storedPort > 0 && IsPortAvailable(storedPort)) + { + Debug.Log($"Using stored port {storedPort}"); + return storedPort; + } + + // If no stored port or stored port is unavailable, find a new one + int newPort = FindAvailablePort(); + SavePort(newPort); + return newPort; + } + + /// + /// Discover and save a new available port (used by Auto-Connect button) + /// + /// New available port + public static int DiscoverNewPort() + { + int newPort = FindAvailablePort(); + SavePort(newPort); + Debug.Log($"Discovered and saved new port: {newPort}"); + return newPort; + } + + /// + /// Find an available port starting from the default port + /// + /// Available port number + private static int FindAvailablePort() + { + // Always try default port first + if (IsPortAvailable(DefaultPort)) + { + Debug.Log($"Using default port {DefaultPort}"); + return DefaultPort; + } + + Debug.Log($"Default port {DefaultPort} is in use, searching for alternative..."); + + // Search for alternatives + for (int port = DefaultPort + 1; port < DefaultPort + MaxPortAttempts; port++) + { + if (IsPortAvailable(port)) + { + Debug.Log($"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 + /// + /// 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; + } + } + + /// + /// 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 = Path.Combine(registryDir, RegistryFileName); + string json = JsonConvert.SerializeObject(portConfig, Formatting.Indented); + File.WriteAllText(registryFile, json); + + Debug.Log($"Saved port {port} to storage"); + } + catch (Exception ex) + { + Debug.LogWarning($"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 = Path.Combine(GetRegistryDirectory(), RegistryFileName); + + if (!File.Exists(registryFile)) + { + return 0; + } + + string json = File.ReadAllText(registryFile); + var portConfig = JsonConvert.DeserializeObject(json); + + return portConfig?.unity_port ?? 0; + } + catch (Exception ex) + { + Debug.LogWarning($"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 = Path.Combine(GetRegistryDirectory(), RegistryFileName); + + if (!File.Exists(registryFile)) + { + return null; + } + + string json = File.ReadAllText(registryFile); + return JsonConvert.DeserializeObject(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"); + } + } +} \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Helpers/PortManager.cs.meta b/UnityMcpBridge/Editor/Helpers/PortManager.cs.meta new file mode 100644 index 00000000..ee3f667c --- /dev/null +++ b/UnityMcpBridge/Editor/Helpers/PortManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a1b2c3d4e5f6789012345678901234ab +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/UnityMcpBridge/Editor/UnityMcpBridge.cs b/UnityMcpBridge/Editor/UnityMcpBridge.cs index 2242cd62..4f3a6082 100644 --- a/UnityMcpBridge/Editor/UnityMcpBridge.cs +++ b/UnityMcpBridge/Editor/UnityMcpBridge.cs @@ -25,9 +25,40 @@ private static Dictionary< string, (string commandJson, TaskCompletionSource tcs) > commandQueue = new(); - private static readonly int unityPort = 6400; // Hardcoded port + private static int currentUnityPort = 6400; // Dynamic port, starts with default + private static bool isAutoConnectMode = false; public static bool IsRunning => isRunning; + public static int GetCurrentPort() => currentUnityPort; + public static bool IsAutoConnectMode() => isAutoConnectMode; + + /// + /// Start with Auto-Connect mode - discovers new port and saves it + /// + public static void StartAutoConnect() + { + Stop(); // Stop current connection + + try + { + // Discover new port and save it + currentUnityPort = PortManager.DiscoverNewPort(); + + listener = new TcpListener(IPAddress.Loopback, currentUnityPort); + listener.Start(); + isRunning = true; + isAutoConnectMode = true; + + Debug.Log($"UnityMcpBridge auto-connected on port {currentUnityPort}"); + Task.Run(ListenerLoop); + EditorApplication.update += ProcessCommands; + } + catch (Exception ex) + { + Debug.LogError($"Auto-connect failed: {ex.Message}"); + throw; + } + } public static bool FolderExists(string path) { @@ -74,10 +105,14 @@ public static void Start() try { - listener = new TcpListener(IPAddress.Loopback, unityPort); + // Use PortManager to get available port with automatic fallback + currentUnityPort = PortManager.GetPortWithFallback(); + + listener = new TcpListener(IPAddress.Loopback, currentUnityPort); listener.Start(); isRunning = true; - Debug.Log($"UnityMcpBridge started on port {unityPort}."); + 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; @@ -87,7 +122,7 @@ public static void Start() if (ex.SocketErrorCode == SocketError.AddressAlreadyInUse) { Debug.LogError( - $"Port {unityPort} is already in use. Ensure no other instances are running or change the port." + $"Port {currentUnityPort} is already in use. This should not happen with dynamic port allocation." ); } else diff --git a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs index 561cd39d..62c919d8 100644 --- a/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs +++ b/UnityMcpBridge/Editor/Windows/UnityMcpEditorWindow.cs @@ -18,8 +18,7 @@ public class UnityMcpEditorWindow : EditorWindow private Vector2 scrollPosition; private string pythonServerInstallationStatus = "Not Installed"; private Color pythonServerInstallationStatusColor = Color.red; - private const int unityPort = 6400; // Hardcoded Unity port - private const int mcpPort = 6500; // Hardcoded MCP port + private const int mcpPort = 6500; // MCP port (still hardcoded for MCP server) private readonly McpClients mcpClients = new(); // Script validation settings @@ -45,6 +44,7 @@ private void OnEnable() { UpdatePythonServerInstallationStatus(); + // Refresh bridge status isUnityBridgeRunning = UnityMcpBridge.IsRunning; foreach (McpClient mcpClient in mcpClients.clients) { @@ -210,11 +210,42 @@ private void DrawServerStatusSection() EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(5); + + // Connection mode and Auto-Connect button + EditorGUILayout.BeginHorizontal(); + + bool isAutoMode = UnityMcpBridge.IsAutoConnectMode(); + GUIStyle modeStyle = new GUIStyle(EditorStyles.miniLabel) { fontSize = 11 }; + EditorGUILayout.LabelField($"Mode: {(isAutoMode ? "Auto" : "Standard")}", modeStyle); + + // Auto-Connect button + if (GUILayout.Button(isAutoMode ? "Connected ✓" : "Auto-Connect", GUILayout.Width(100), GUILayout.Height(24))) + { + if (!isAutoMode) + { + try + { + UnityMcpBridge.StartAutoConnect(); + // Update UI state + isUnityBridgeRunning = UnityMcpBridge.IsRunning; + Repaint(); + } + catch (Exception ex) + { + EditorUtility.DisplayDialog("Auto-Connect Failed", ex.Message, "OK"); + } + } + } + + EditorGUILayout.EndHorizontal(); + + // Current ports display + int currentUnityPort = UnityMcpBridge.GetCurrentPort(); GUIStyle portStyle = new GUIStyle(EditorStyles.miniLabel) { fontSize = 11 }; - EditorGUILayout.LabelField($"Ports: Unity {unityPort}, MCP {mcpPort}", portStyle); + EditorGUILayout.LabelField($"Ports: Unity {currentUnityPort}, MCP {mcpPort}", portStyle); EditorGUILayout.Space(5); EditorGUILayout.EndVertical(); } diff --git a/UnityMcpServer/src/config.py b/UnityMcpServer/src/config.py index 58f6f846..c42437a7 100644 --- a/UnityMcpServer/src/config.py +++ b/UnityMcpServer/src/config.py @@ -15,7 +15,7 @@ class ServerConfig: mcp_port: int = 6500 # Connection settings - connection_timeout: float = 86400.0 # 24 hours timeout + connection_timeout: float = 600.0 # 10 minutes timeout buffer_size: int = 16 * 1024 * 1024 # 16MB buffer # Logging settings diff --git a/UnityMcpServer/src/port_discovery.py b/UnityMcpServer/src/port_discovery.py new file mode 100644 index 00000000..a0dfe961 --- /dev/null +++ b/UnityMcpServer/src/port_discovery.py @@ -0,0 +1,69 @@ +""" +Port discovery utility for Unity MCP Server. +Reads port configuration saved by Unity Bridge. +""" + +import json +import os +import logging +from pathlib import Path +from typing import Optional + +logger = logging.getLogger("unity-mcp-server") + +class PortDiscovery: + """Handles port discovery from Unity Bridge registry""" + + REGISTRY_FILE = "unity-mcp-port.json" + + @staticmethod + def get_registry_path() -> Path: + """Get the path to the port registry file""" + return Path.home() / ".unity-mcp" / PortDiscovery.REGISTRY_FILE + + @staticmethod + def discover_unity_port() -> int: + """ + Discover Unity port from registry file with fallback to default + + Returns: + Port number to connect to + """ + registry_file = PortDiscovery.get_registry_path() + + if registry_file.exists(): + 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 + + except Exception as e: + logger.warning(f"Could not read port registry: {e}") + + # Fallback to default port + logger.info("No port registry found, using default port 6400") + return 6400 + + @staticmethod + def get_port_config() -> Optional[dict]: + """ + Get the full port configuration from registry + + Returns: + Port configuration dict or None if not found + """ + registry_file = PortDiscovery.get_registry_path() + + if not registry_file.exists(): + 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 diff --git a/UnityMcpServer/src/unity_connection.py b/UnityMcpServer/src/unity_connection.py index 252b5048..da88d9bd 100644 --- a/UnityMcpServer/src/unity_connection.py +++ b/UnityMcpServer/src/unity_connection.py @@ -4,6 +4,7 @@ from dataclasses import dataclass from typing import Dict, Any from config import config +from port_discovery import PortDiscovery # Configure logging using settings from config logging.basicConfig( @@ -16,8 +17,13 @@ class UnityConnection: """Manages the socket connection to the Unity Editor.""" host: str = config.unity_host - port: int = config.unity_port + port: int = None # Will be set dynamically sock: socket.socket = None # Socket for Unity communication + + def __post_init__(self): + """Set port from discovery if not explicitly provided""" + if self.port is None: + self.port = PortDiscovery.discover_unity_port() def connect(self) -> bool: """Establish a connection to the Unity Editor."""