Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion MCPForUnity/Editor/Helpers/PortManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,17 +119,41 @@ private static int FindAvailablePort()
/// <returns>True if port is available</returns>
public static bool IsPortAvailable(int port)
{
// Start with quick loopback check
try
{
var testListener = new TcpListener(IPAddress.Loopback, port);
testListener.Start();
testListener.Stop();
return true;
}
catch (SocketException)
{
return false;
}

#if UNITY_EDITOR_OSX
// On macOS, the OS might report the port as available (SO_REUSEADDR) even if another process
// is using it, unless we also check active connections or try a stricter bind.
// Double check by trying to Connect to it. If we CAN connect, it's NOT available.
try
{
using var client = new TcpClient();
var connectTask = client.ConnectAsync(IPAddress.Loopback, port);
// If we connect successfully, someone is listening -> Not available
if (connectTask.Wait(50) && client.Connected)
{
if (IsDebugEnabled()) McpLog.Info($"[PortManager] Port {port} bind succeeded but connection also succeeded -> Not available (Conflict).");
return false;
}
}
Comment on lines +140 to +148
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Resource leak: TcpClient is created without using statement, but client.ConnectAsync() returns a Task that's awaited synchronously with .Wait(50). If connection times out or succeeds, the TcpClient may not be properly disposed before entering the catch block or returning.

Suggested change
using var client = new TcpClient();
var connectTask = client.ConnectAsync(IPAddress.Loopback, port);
// If we connect successfully, someone is listening -> Not available
if (connectTask.Wait(50) && client.Connected)
{
if (IsDebugEnabled()) McpLog.Info($"[PortManager] Port {port} bind succeeded but connection also succeeded -> Not available (Conflict).");
return false;
}
}
try
{
using var client = new TcpClient();
var connectTask = client.ConnectAsync(IPAddress.Loopback, port);
// If we connect successfully, someone is listening -> Not available
if (connectTask.Wait(50) && client.Connected)
{
if (IsDebugEnabled()) McpLog.Info($"[PortManager] Port {port} bind succeeded but connection also succeeded -> Not available (Conflict).");
return false;
}
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: MCPForUnity/Editor/Helpers/PortManager.cs
Line: 140:148

Comment:
**style:** Resource leak: `TcpClient` is created without using statement, but `client.ConnectAsync()` returns a `Task` that's awaited synchronously with `.Wait(50)`. If connection times out or succeeds, the `TcpClient` may not be properly disposed before entering the `catch` block or returning.

```suggestion
            try
            {
                using var client = new TcpClient();
                var connectTask = client.ConnectAsync(IPAddress.Loopback, port);
                // If we connect successfully, someone is listening -> Not available
                if (connectTask.Wait(50) && client.Connected)
                {
                    if (IsDebugEnabled()) McpLog.Info($"[PortManager] Port {port} bind succeeded but connection also succeeded -> Not available (Conflict).");
                    return false;
                }
            }
```

How can I resolve this? If you propose a fix, please make it concise.

catch
{
// Connection failed -> likely available (or firewall blocked, but we assume available)
if (IsDebugEnabled()) McpLog.Info($"[PortManager] Port {port} connection failed -> likely available.");
}
#endif

return true;
}

/// <summary>
Expand Down
78 changes: 37 additions & 41 deletions MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -306,26 +306,7 @@ public static void Start()
{
try
{
listener = new TcpListener(IPAddress.Loopback, currentUnityPort);
listener.Server.SetSocketOption(
SocketOptionLevel.Socket,
SocketOptionName.ReuseAddress,
true
);
#if UNITY_EDITOR_WIN
try
{
listener.ExclusiveAddressUse = false;
}
catch { }
#endif
try
{
listener.Server.LingerState = new LingerOption(true, 0);
}
catch (Exception)
{
}
listener = CreateConfiguredListener(currentUnityPort);
listener.Start();
break;
}
Expand Down Expand Up @@ -355,7 +336,14 @@ public static void Start()
}
catch { }

currentUnityPort = PortManager.GetPortWithFallback();
currentUnityPort = PortManager.DiscoverNewPort();

// Persist the new port so next time we start on this port
try
{
EditorPrefs.SetInt(EditorPrefKeys.UnitySocketPort, currentUnityPort);
}
catch { }

if (IsDebugEnabled())
{
Expand All @@ -369,26 +357,7 @@ public static void Start()
}
}

listener = new TcpListener(IPAddress.Loopback, currentUnityPort);
listener.Server.SetSocketOption(
SocketOptionLevel.Socket,
SocketOptionName.ReuseAddress,
true
);
#if UNITY_EDITOR_WIN
try
{
listener.ExclusiveAddressUse = false;
}
catch { }
#endif
try
{
listener.Server.LingerState = new LingerOption(true, 0);
}
catch (Exception)
{
}
listener = CreateConfiguredListener(currentUnityPort);
listener.Start();
break;
}
Expand Down Expand Up @@ -416,6 +385,33 @@ public static void Start()
}
}

private static TcpListener CreateConfiguredListener(int port)
{
var newListener = new TcpListener(IPAddress.Loopback, port);
#if !UNITY_EDITOR_OSX
newListener.Server.SetSocketOption(
SocketOptionLevel.Socket,
SocketOptionName.ReuseAddress,
true
);
#endif
#if UNITY_EDITOR_WIN
try
{
newListener.ExclusiveAddressUse = false;
}
catch { }
#endif
try
{
newListener.Server.LingerState = new LingerOption(true, 0);
}
catch (Exception)
{
}
return newListener;
}

public static void Stop()
{
Task toWait = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,24 +173,30 @@ public void UpdateConnectionStatus()
statusIndicator.RemoveFromClassList("disconnected");
statusIndicator.AddToClassList("connected");
connectionToggleButton.text = "End Session";

// Force the UI to reflect the actual port being used
unityPortField.value = bridgeService.CurrentPort.ToString();
unityPortField.SetEnabled(false);
}
else
{
connectionStatusLabel.text = "No Session";
statusIndicator.RemoveFromClassList("connected");
statusIndicator.AddToClassList("disconnected");
connectionToggleButton.text = "Start Session";

unityPortField.SetEnabled(true);

healthStatusLabel.text = HealthStatusUnknown;
healthIndicator.RemoveFromClassList("healthy");
healthIndicator.RemoveFromClassList("warning");
healthIndicator.AddToClassList("unknown");
}

int savedPort = EditorPrefs.GetInt(EditorPrefKeys.UnitySocketPort, 0);
if (savedPort == 0)
{
unityPortField.value = bridgeService.CurrentPort.ToString();
int savedPort = EditorPrefs.GetInt(EditorPrefKeys.UnitySocketPort, 0);
if (savedPort == 0)
{
unityPortField.value = bridgeService.CurrentPort.ToString();
}
}
}

Expand Down
1 change: 1 addition & 0 deletions Server/src/services/tools/debug_request_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def debug_request_context(ctx: Context) -> dict[str, Any]:
active_instance = middleware.get_active_instance(ctx)

# Debugging middleware internals
# NOTE: These fields expose internal implementation details and may change between versions.
with middleware._lock:
all_keys = list(middleware._active_by_key.keys())

Expand Down
12 changes: 11 additions & 1 deletion Server/src/transport/unity_instance_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ async def on_call_tool(self, context: MiddlewareContext, call_next):
# We only need session_id for HTTP transport routing.
# For stdio, we just need the instance ID.
session_id = await PluginHub._resolve_session_id(active_instance)
except Exception as exc:
except (ConnectionError, ValueError, KeyError, TimeoutError) as exc:
# If resolution fails, it means the Unity instance is not reachable via HTTP/WS.
# If we are in stdio mode, this might still be fine if the user is just setting state?
# But usually if PluginHub is configured, we expect it to work.
Expand All @@ -115,6 +115,16 @@ async def on_call_tool(self, context: MiddlewareContext, call_next):
exc,
exc_info=True,
)
except Exception as exc:
# Re-raise unexpected system exceptions to avoid swallowing critical failures
if isinstance(exc, (SystemExit, KeyboardInterrupt)):
raise
logger.error(
"Unexpected error during PluginHub session resolution for %s: %s",
active_instance,
exc,
exc_info=True
)

ctx.set_state("unity_instance", active_instance)
if session_id is not None:
Expand Down
Loading