Skip to content

Commit d8d06cb

Browse files
committed
fix: make stdio bridge command timeout configurable (default 5m)
Long-running tool calls (asset imports, test runs, batched edits) were cut off ~30-90s into execution, so the task could never finish. On the stdio transport this was governed by hardcoded values on both hops: - Unity side: StdioBridgeHost.FrameIOTimeoutMs (30s const) capped every command's execution and frame I/O; on timeout the client reconnected and re-sent, which force-closed the prior client and made the bridge restart on a new port (the repeated "StdioBridgeHost started on port 6400/6402" churn). - Server side: ServerConfig.connection_timeout (30s socket recv) and command_total_timeout (90s cross-retry ceiling) cut the command off first. Unlike the WebSocket transport (WebSocketTransportClient reads a per-call timeout off the wire), the stdio bridge had no way to raise these. Make all three configurable with a 5-minute default: - FrameIOTimeoutMs: 30s -> 300s, env UNITY_MCP_STDIO_COMMAND_TIMEOUT_MS. ReceiveTimeout now scales with it (max(60s, timeout)). - connection_timeout: 30s -> 300s, env UNITY_MCP_CONNECTION_TIMEOUT. - command_total_timeout: 90s -> 600s, env UNITY_MCP_COMMAND_TOTAL_TIMEOUT. Invalid/non-positive env values fall back to the default so a bad override can't disable the timeout. Updates the config characterization test to the new defaults.
1 parent 9f84072 commit d8d06cb

3 files changed

Lines changed: 60 additions & 7 deletions

File tree

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

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,33 @@ public static class StdioBridgeHost
5858
private static int currentUnityPort = 6400;
5959
private static bool isAutoConnectMode = false;
6060
private const ulong MaxFrameBytes = 64UL * 1024 * 1024;
61-
private const int FrameIOTimeoutMs = 30000;
61+
// Command/frame I/O timeout for the stdio bridge TCP hop. Previously a
62+
// hardcoded 30s const, which cut off long-running tool calls mid-execution
63+
// (the client would then reconnect and re-send, causing the bridge to
64+
// restart on a new port). Now defaults to 5 minutes and is overridable via
65+
// the UNITY_MCP_STDIO_COMMAND_TIMEOUT_MS environment variable.
66+
private const int DefaultFrameIOTimeoutMs = 300000;
67+
private static readonly int FrameIOTimeoutMs = ResolveFrameIOTimeoutMs();
6268
private static readonly Stopwatch _uptime = Stopwatch.StartNew();
6369
private static volatile int _consecutiveTimeouts = 0;
6470
private static bool _processCommandsHooked = false;
6571

72+
private static int ResolveFrameIOTimeoutMs()
73+
{
74+
try
75+
{
76+
string raw = Environment.GetEnvironmentVariable("UNITY_MCP_STDIO_COMMAND_TIMEOUT_MS");
77+
if (!string.IsNullOrWhiteSpace(raw)
78+
&& int.TryParse(raw.Trim(), out int ms)
79+
&& ms > 0)
80+
{
81+
return ms;
82+
}
83+
}
84+
catch { /* fall through to default */ }
85+
return DefaultFrameIOTimeoutMs;
86+
}
87+
6688
private static void IoInfo(string s) { McpLog.Info(s, always: false); }
6789

6890
private static bool IsDebugEnabled()
@@ -465,7 +487,9 @@ private static async Task ListenerLoopAsync(CancellationToken token)
465487
true
466488
);
467489

468-
client.ReceiveTimeout = 60000;
490+
// Keep the socket receive timeout at least as long as the command
491+
// timeout so it never fires before a long-running tool call completes.
492+
client.ReceiveTimeout = Math.Max(60000, FrameIOTimeoutMs);
469493

470494
_ = Task.Run(() => HandleClientAsync(client, token), token);
471495
}
@@ -825,7 +849,7 @@ private static void ProcessCommands()
825849

826850
// Evict commands stuck with IsExecuting=true for too long (e.g. from pre-reload state).
827851
long nowMs = _uptime.ElapsedMilliseconds;
828-
const long staleThresholdMs = 2L * FrameIOTimeoutMs; // 60s
852+
long staleThresholdMs = 2L * FrameIOTimeoutMs; // 2x the command timeout
829853
List<string> staleIds = null;
830854
foreach (var kvp in commandQueue)
831855
{

Server/src/core/config.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,25 @@
33
This file contains all configurable parameters for the server.
44
"""
55

6-
from dataclasses import dataclass
6+
import os
7+
from dataclasses import dataclass, field
8+
9+
10+
def _env_float(name: str, default: float) -> float:
11+
"""Read a positive float from an environment variable, falling back to default.
12+
13+
Invalid or non-positive values are ignored so a bad override can't disable
14+
the timeout entirely.
15+
"""
16+
raw = os.environ.get(name)
17+
if raw:
18+
try:
19+
value = float(raw.strip())
20+
if value > 0:
21+
return value
22+
except (TypeError, ValueError):
23+
pass
24+
return default
725

826

927
@dataclass
@@ -31,9 +49,19 @@ class ServerConfig:
3149
api_key_service_token: str | None = None # The token value
3250

3351
# Connection settings
34-
connection_timeout: float = 30.0
52+
# Socket receive timeout for a single Unity command (seconds). Raised from the
53+
# historical 30s so long-running tools (e.g. imports, test runs, batched edits)
54+
# aren't cut off mid-execution, which previously forced a reconnect + re-send and
55+
# made the Unity stdio bridge restart. Override via UNITY_MCP_CONNECTION_TIMEOUT.
56+
connection_timeout: float = field(
57+
default_factory=lambda: _env_float("UNITY_MCP_CONNECTION_TIMEOUT", 300.0)
58+
)
3559
# Hard ceiling on a command's total time across all retries (wedged-socket guard).
36-
command_total_timeout: float = 90.0
60+
# Kept above connection_timeout so a single slow-but-progressing command still fits.
61+
# Override via UNITY_MCP_COMMAND_TOTAL_TIMEOUT.
62+
command_total_timeout: float = field(
63+
default_factory=lambda: _env_float("UNITY_MCP_COMMAND_TOTAL_TIMEOUT", 600.0)
64+
)
3765
buffer_size: int = 16 * 1024 * 1024 # 16MB buffer
3866

3967
# STDIO framing behaviour

Server/tests/test_core_infrastructure_characterization.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -661,7 +661,8 @@ def test_config_default_values(self):
661661
assert config.unity_host == "127.0.0.1"
662662
assert config.unity_port == 6400
663663
assert config.mcp_port == 6500
664-
assert config.connection_timeout == 30.0
664+
assert config.connection_timeout == 300.0
665+
assert config.command_total_timeout == 600.0
665666
assert config.buffer_size == 16 * 1024 * 1024
666667
assert config.require_framing is True
667668
assert config.handshake_timeout == 1.0

0 commit comments

Comments
 (0)