Skip to content

Commit ee41cd1

Browse files
authored
Merge pull request #688 from dsarno/fix/localhost-ipv6-resolution
Merge after validation on combined beta-features branch (all 640 Python + 621 Unity tests passing).
2 parents 5608665 + 5610890 commit ee41cd1

13 files changed

Lines changed: 604 additions & 102 deletions

MCPForUnity/Editor/Helpers/HttpEndpointUtility.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public static class HttpEndpointUtility
1818
{
1919
private const string LocalPrefKey = EditorPrefKeys.HttpBaseUrl;
2020
private const string RemotePrefKey = EditorPrefKeys.HttpRemoteBaseUrl;
21-
private const string DefaultLocalBaseUrl = "http://localhost:8080";
21+
private const string DefaultLocalBaseUrl = "http://127.0.0.1:8080";
2222
private const string DefaultRemoteBaseUrl = "";
2323

2424
/// <summary>

MCPForUnity/Editor/Services/HttpBridgeReloadHandler.cs

Lines changed: 60 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ namespace MCPForUnity.Editor.Services
1414
[InitializeOnLoad]
1515
internal static class HttpBridgeReloadHandler
1616
{
17+
private static readonly TimeSpan[] ResumeRetrySchedule =
18+
{
19+
TimeSpan.Zero,
20+
TimeSpan.FromSeconds(1),
21+
TimeSpan.FromSeconds(3),
22+
TimeSpan.FromSeconds(5),
23+
TimeSpan.FromSeconds(10),
24+
TimeSpan.FromSeconds(30)
25+
};
26+
1727
static HttpBridgeReloadHandler()
1828
{
1929
AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload;
@@ -38,14 +48,9 @@ private static void OnBeforeAssemblyReload()
3848

3949
if (shouldResume)
4050
{
41-
var stopTask = transport.StopAsync(TransportMode.Http);
42-
stopTask.ContinueWith(t =>
43-
{
44-
if (t.IsFaulted && t.Exception != null)
45-
{
46-
McpLog.Warn($"Error stopping MCP bridge before reload: {t.Exception.GetBaseException().Message}");
47-
}
48-
}, TaskScheduler.Default);
51+
// beforeAssemblyReload is synchronous; force a synchronous teardown so we do not
52+
// leave an orphaned socket due to an unfinished async close handshake.
53+
transport.ForceStop(TransportMode.Http);
4954
}
5055
}
5156
catch (Exception ex)
@@ -90,56 +95,69 @@ private static void OnAfterAssemblyReload()
9095

9196
if (!isCompiling)
9297
{
93-
try
98+
_ = ResumeHttpWithRetriesAsync();
99+
return;
100+
}
101+
102+
// Fallback when compiling: schedule on the editor loop
103+
EditorApplication.delayCall += () =>
104+
{
105+
_ = ResumeHttpWithRetriesAsync();
106+
};
107+
}
108+
109+
private static async Task ResumeHttpWithRetriesAsync()
110+
{
111+
Exception lastException = null;
112+
113+
for (int i = 0; i < ResumeRetrySchedule.Length; i++)
114+
{
115+
int attempt = i + 1;
116+
McpLog.Debug($"[HTTP Reload] Resume attempt {attempt}/{ResumeRetrySchedule.Length}");
117+
118+
TimeSpan delay = ResumeRetrySchedule[i];
119+
if (delay > TimeSpan.Zero)
94120
{
95-
var startTask = MCPServiceLocator.TransportManager.StartAsync(TransportMode.Http);
96-
startTask.ContinueWith(t =>
97-
{
98-
if (t.IsFaulted)
99-
{
100-
var baseEx = t.Exception?.GetBaseException();
101-
McpLog.Warn($"Failed to resume HTTP MCP bridge after domain reload: {baseEx?.Message}");
102-
return;
103-
}
104-
bool started = t.Result;
105-
if (!started)
106-
{
107-
McpLog.Warn("Failed to resume HTTP MCP bridge after domain reload");
108-
}
109-
else
110-
{
111-
MCPForUnityEditorWindow.RequestHealthVerification();
112-
}
113-
}, TaskScheduler.Default);
114-
return;
121+
McpLog.Debug($"[HTTP Reload] Waiting {delay.TotalSeconds:0.#}s before resume attempt {attempt}");
122+
try { await Task.Delay(delay); }
123+
catch { return; }
115124
}
116-
catch (Exception ex)
125+
126+
// Abort retries if the user switched transports while we were waiting.
127+
if (!EditorConfigurationCache.Instance.UseHttpTransport)
117128
{
118-
McpLog.Error($"Error resuming HTTP MCP bridge: {ex.Message}");
119129
return;
120130
}
121-
}
122131

123-
// Fallback when compiling: schedule on the editor loop
124-
EditorApplication.delayCall += async () =>
125-
{
126132
try
127133
{
128134
bool started = await MCPServiceLocator.TransportManager.StartAsync(TransportMode.Http);
129-
if (!started)
130-
{
131-
McpLog.Warn("Failed to resume HTTP MCP bridge after domain reload");
132-
}
133-
else
135+
if (started)
134136
{
137+
McpLog.Debug($"[HTTP Reload] Resume succeeded on attempt {attempt}");
135138
MCPForUnityEditorWindow.RequestHealthVerification();
139+
return;
136140
}
141+
142+
var state = MCPServiceLocator.TransportManager.GetState(TransportMode.Http);
143+
string reason = string.IsNullOrWhiteSpace(state?.Error) ? "no error detail" : state.Error;
144+
McpLog.Debug($"[HTTP Reload] Resume attempt {attempt} failed: {reason}");
137145
}
138146
catch (Exception ex)
139147
{
140-
McpLog.Error($"Error resuming HTTP MCP bridge: {ex.Message}");
148+
lastException = ex;
149+
McpLog.Debug($"[HTTP Reload] Resume attempt {attempt} threw: {ex.Message}");
141150
}
142-
};
151+
}
152+
153+
if (lastException != null)
154+
{
155+
McpLog.Warn($"Failed to resume HTTP MCP bridge after domain reload: {lastException.Message}");
156+
}
157+
else
158+
{
159+
McpLog.Warn("Failed to resume HTTP MCP bridge after domain reload");
160+
}
143161
}
144162
}
145163
}

MCPForUnity/Editor/Services/ServerManagementService.cs

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -456,22 +456,7 @@ private static bool TryConnectToLocalPort(string host, int port, int timeoutMs)
456456
{
457457
try
458458
{
459-
if (string.IsNullOrEmpty(host))
460-
{
461-
host = "127.0.0.1";
462-
}
463-
464-
var hosts = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { host };
465-
if (host == "localhost" || host == "0.0.0.0")
466-
{
467-
hosts.Add("127.0.0.1");
468-
}
469-
if (host == "::" || host == "0:0:0:0:0:0:0:0")
470-
{
471-
hosts.Add("::1");
472-
}
473-
474-
foreach (var target in hosts)
459+
foreach (string target in BuildLocalProbeHosts(host))
475460
{
476461
try
477462
{
@@ -498,6 +483,55 @@ private static bool TryConnectToLocalPort(string host, int port, int timeoutMs)
498483
return false;
499484
}
500485

486+
private static IReadOnlyList<string> BuildLocalProbeHosts(string host)
487+
{
488+
if (string.IsNullOrWhiteSpace(host))
489+
{
490+
host = "127.0.0.1";
491+
}
492+
else
493+
{
494+
host = host.Trim();
495+
}
496+
497+
var hosts = new List<string>();
498+
AddHostCandidate(hosts, host);
499+
500+
if (string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase))
501+
{
502+
// Probe both loopback families for localhost to avoid false negatives on systems where
503+
// localhost resolution prefers an address family different from the server bind.
504+
AddHostCandidate(hosts, "127.0.0.1");
505+
AddHostCandidate(hosts, "::1");
506+
}
507+
else if (string.Equals(host, "0.0.0.0", StringComparison.OrdinalIgnoreCase))
508+
{
509+
AddHostCandidate(hosts, "127.0.0.1");
510+
}
511+
else if (string.Equals(host, "::", StringComparison.OrdinalIgnoreCase) ||
512+
string.Equals(host, "0:0:0:0:0:0:0:0", StringComparison.OrdinalIgnoreCase))
513+
{
514+
AddHostCandidate(hosts, "::1");
515+
}
516+
517+
return hosts;
518+
}
519+
520+
private static void AddHostCandidate(List<string> hosts, string candidate)
521+
{
522+
if (string.IsNullOrWhiteSpace(candidate))
523+
{
524+
return;
525+
}
526+
527+
if (hosts.Any(existing => string.Equals(existing, candidate, StringComparison.OrdinalIgnoreCase)))
528+
{
529+
return;
530+
}
531+
532+
hosts.Add(candidate);
533+
}
534+
501535
private bool StopLocalHttpServerInternal(bool quiet, int? portOverride = null, bool allowNonLocalUrl = false)
502536
{
503537
string httpUrl = HttpEndpointUtility.GetLocalBaseUrl();

MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,27 @@ private static void OnBeforeAssemblyReload()
3535
if (shouldResume)
3636
{
3737
EditorPrefs.SetBool(EditorPrefKeys.ResumeStdioAfterReload, true);
38+
}
39+
else
40+
{
41+
EditorPrefs.DeleteKey(EditorPrefKeys.ResumeStdioAfterReload);
42+
}
3843

39-
// Stop only the stdio bridge; leave HTTP untouched if it is running concurrently.
44+
if (isRunning)
45+
{
46+
// Stop only stdio before reload. This is centralized here so resume-flag updates
47+
// and teardown cannot race each other via separate beforeAssemblyReload handlers.
4048
var stopTask = MCPServiceLocator.TransportManager.StopAsync(TransportMode.Stdio);
41-
42-
// Wait for stop to complete (which deletes the status file)
4349
try { stopTask.Wait(500); } catch { }
4450

45-
// Write reloading status so clients don't think we vanished
46-
StdioBridgeHost.WriteHeartbeat(true, "reloading");
51+
// Legacy safety: stdio may have been started outside TransportManager state.
52+
try { StdioBridgeHost.Stop(); } catch { }
4753
}
48-
else
54+
55+
if (shouldResume)
4956
{
50-
EditorPrefs.DeleteKey(EditorPrefKeys.ResumeStdioAfterReload);
57+
// Write reloading status so clients don't think we vanished.
58+
StdioBridgeHost.WriteHeartbeat(true, "reloading");
5159
}
5260
}
5361
catch (Exception ex)

MCPForUnity/Editor/Services/Transport/TransportManager.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,41 @@ public TransportState GetState(TransportMode mode)
128128

129129
public bool IsRunning(TransportMode mode) => GetState(mode).IsConnected;
130130

131+
/// <summary>
132+
/// Synchronous teardown for shutdown/reload hooks where async awaits are not possible.
133+
/// </summary>
134+
public void ForceStop(TransportMode mode)
135+
{
136+
IMcpTransportClient client = GetClient(mode);
137+
string transportName = client?.TransportName ?? mode.ToString().ToLowerInvariant();
138+
139+
if (client == null)
140+
{
141+
UpdateState(mode, TransportState.Disconnected(transportName));
142+
return;
143+
}
144+
145+
try
146+
{
147+
if (client is WebSocketTransportClient wsClient)
148+
{
149+
wsClient.ForceStop();
150+
}
151+
else
152+
{
153+
client.StopAsync().GetAwaiter().GetResult();
154+
}
155+
}
156+
catch (Exception ex)
157+
{
158+
McpLog.Warn($"Error while force-stopping transport {transportName}: {ex.Message}");
159+
}
160+
finally
161+
{
162+
UpdateState(mode, TransportState.Disconnected(transportName));
163+
}
164+
}
165+
131166
private void UpdateState(TransportMode mode, TransportState state)
132167
{
133168
switch (mode)

MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -266,8 +266,8 @@ public static void Start()
266266

267267
LogBreadcrumb("Start");
268268

269-
const int maxImmediateRetries = 3;
270-
const int retrySleepMs = 75;
269+
const int maxImmediateRetries = 10;
270+
const int retrySleepMs = 200;
271271
int attempt = 0;
272272
for (; ; )
273273
{
@@ -355,19 +355,12 @@ public static void Start()
355355
private static TcpListener CreateConfiguredListener(int port)
356356
{
357357
var newListener = new TcpListener(IPAddress.Loopback, port);
358-
#if !UNITY_EDITOR_OSX
358+
#if UNITY_EDITOR_OSX
359359
newListener.Server.SetSocketOption(
360360
SocketOptionLevel.Socket,
361361
SocketOptionName.ReuseAddress,
362362
true
363363
);
364-
#endif
365-
#if UNITY_EDITOR_WIN
366-
try
367-
{
368-
newListener.ExclusiveAddressUse = false;
369-
}
370-
catch { }
371364
#endif
372365
try
373366
{
@@ -398,6 +391,7 @@ public static void Stop()
398391
try { cancel?.Cancel(); } catch { }
399392

400393
try { listener?.Stop(); } catch { }
394+
try { listener?.Server?.Dispose(); } catch { }
401395
listener = null;
402396

403397
toWait = listenerTask;
@@ -422,7 +416,7 @@ public static void Stop()
422416

423417
if (toWait != null)
424418
{
425-
try { toWait.Wait(100); } catch { }
419+
try { toWait.Wait(2000); } catch { }
426420
}
427421

428422
try { EditorApplication.update -= ProcessCommands; } catch { }

0 commit comments

Comments
 (0)