Merge pull request #176 from Scriptwonder/master
Autoconnect feature to prevent the port from being taken by other applications. Currently still supporting local host, and we will figure out how remote hosting should work. MCP still is a 1:1 service. The newly instantiated port will take over the new port from the commands. Something worth discussing for.main
commit
41f0a57f81
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages dynamic port allocation and persistent storage for Unity MCP Bridge
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <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()
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
/// <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);
|
||||
Debug.Log($"Discovered and saved new port: {newPort}");
|
||||
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))
|
||||
{
|
||||
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}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a specific port is available
|
||||
/// </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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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);
|
||||
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load port from persistent storage
|
||||
/// </summary>
|
||||
/// <returns>Stored port number, or 0 if not found</returns>
|
||||
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<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
|
||||
{
|
||||
string registryFile = Path.Combine(GetRegistryDirectory(), RegistryFileName);
|
||||
|
||||
if (!File.Exists(registryFile))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a1b2c3d4e5f6789012345678901234ab
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -25,9 +25,40 @@ namespace UnityMcpBridge.Editor
|
|||
string,
|
||||
(string commandJson, TaskCompletionSource<string> 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;
|
||||
|
||||
/// <summary>
|
||||
/// Start with Auto-Connect mode - discovers new port and saves it
|
||||
/// </summary>
|
||||
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 @@ namespace UnityMcpBridge.Editor
|
|||
|
||||
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 @@ namespace UnityMcpBridge.Editor
|
|||
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
|
||||
|
|
|
|||
|
|
@ -18,8 +18,7 @@ namespace UnityMcpBridge.Editor.Windows
|
|||
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 @@ namespace UnityMcpBridge.Editor.Windows
|
|||
{
|
||||
UpdatePythonServerInstallationStatus();
|
||||
|
||||
// Refresh bridge status
|
||||
isUnityBridgeRunning = UnityMcpBridge.IsRunning;
|
||||
foreach (McpClient mcpClient in mcpClients.clients)
|
||||
{
|
||||
|
|
@ -210,11 +210,42 @@ namespace UnityMcpBridge.Editor.Windows
|
|||
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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -4,6 +4,7 @@ import logging
|
|||
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 @@ logger = logging.getLogger("unity-mcp-server")
|
|||
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."""
|
||||
|
|
|
|||
Loading…
Reference in New Issue