You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Whenever the Unity WebSocket handshake fails to complete within connectTimeout (5s), the Node server process exits, closing the stdio pipe. From the MCP client's side the entire tool surface disappears mid-session (MCP error -32000: Connection closed).
The package already implements reconnection with exponential backoff — it never gets a chance to run, because the process is dead before the first retry.
Reproduced on 1.2.0 and 1.3.0 (c35f184) — Node v24.16.0, Windows 11, Unity 6.
Root cause
UnityConnection.doConnect() sets a connect-timeout timer:
closeWebSocket() clears the error handler, then terminates:
socket.onerror=null;// <-- removes the only 'error' listener
...
try{socket.terminate();// <-- socket is still CONNECTING}catch(err){ ... }
ws.terminate() on a CONNECTING socket calls abortHandshake(), which emits 'error'. Because onerror was just cleared there is no listener, so Node's EventEmitter throws. The throw originates in a timer tick, so the surrounding try/catch cannot catch it:
Error: WebSocket was closed before the connection was established
at WebSocket.terminate (node_modules/ws/lib/websocket.js:489:7)
at UnityConnection.closeWebSocket (build/unity/unityConnection.js:396:20)
at Timeout.<anonymous> (build/unity/unityConnection.js:174:26)
at listOnTimeout (node:internal/timers:605:17)
It surfaces as an uncaughtException, and index.ts treats anything that isn't EPIPE/EOF/ERR_USE_AFTER_CLOSE as fatal:
utils/logger.ts discards all output unless LOGGING=true or LOGGING_FILE=true. With neither set — the default when an MCP client launches the server over stdio — the crash reason goes to a no-op logger and the process dies completely silently. I only got the stack by re-running with LOGGING_FILE=true plus my own uncaughtException handler.
In normal use the trigger is routine: any domain reload that outlasts the 5s connect timeout.
Suggested fix
Keep an error listener attached so abortHandshake's emit has somewhere to land:
socket.onopen=null;socket.onmessage=null;socket.onclose=null;socket.removeAllListeners('pong');socket.onerror=()=>{};// terminate() on a CONNECTING socket emits 'error';// an EventEmitter with no 'error' listener throws.try{socket.terminate();}catch(err){ ... }
Worth considering alongside it: a fault originating in the Unity connection is recoverable by design, given the reconnect machinery already present. Treating every such fault as fatal turns a transient editor hiccup into a lost session. Downgrading connection-class faults to a warning would make the existing backoff logic actually reachable.
Workaround
A small launcher that imports the server and then replaces its process-level handlers with tolerant ones. With that in place, a single server process survived two bridge outages (including the #141 wedge) and three Unity restarts in one session, reconnecting each time, with no loss of tools.
Related: #141 — that issue is the Unity-side cause of the timeouts; this issue is about the client-side reaction to them being fatal.
Summary
Whenever the Unity WebSocket handshake fails to complete within
connectTimeout(5s), the Node server process exits, closing the stdio pipe. From the MCP client's side the entire tool surface disappears mid-session (MCP error -32000: Connection closed).The package already implements reconnection with exponential backoff — it never gets a chance to run, because the process is dead before the first retry.
Reproduced on 1.2.0 and 1.3.0 (
c35f184) — Node v24.16.0, Windows 11, Unity 6.Root cause
UnityConnection.doConnect()sets a connect-timeout timer:closeWebSocket()clears the error handler, then terminates:ws.terminate()on aCONNECTINGsocket callsabortHandshake(), which emits'error'. Becauseonerrorwas just cleared there is no listener, so Node's EventEmitter throws. The throw originates in a timer tick, so the surroundingtry/catchcannot catch it:It surfaces as an
uncaughtException, andindex.tstreats anything that isn'tEPIPE/EOF/ERR_USE_AFTER_CLOSEas fatal:Why it's hard to spot
utils/logger.tsdiscards all output unlessLOGGING=trueorLOGGING_FILE=true. With neither set — the default when an MCP client launches the server over stdio — the crash reason goes to a no-op logger and the process dies completely silently. I only got the stack by re-running withLOGGING_FILE=trueplus my ownuncaughtExceptionhandler.Reproduction
node Server~/build/index.jsIn normal use the trigger is routine: any domain reload that outlasts the 5s connect timeout.
Suggested fix
Keep an error listener attached so
abortHandshake's emit has somewhere to land:Worth considering alongside it: a fault originating in the Unity connection is recoverable by design, given the reconnect machinery already present. Treating every such fault as fatal turns a transient editor hiccup into a lost session. Downgrading connection-class faults to a warning would make the existing backoff logic actually reachable.
Workaround
A small launcher that imports the server and then replaces its process-level handlers with tolerant ones. With that in place, a single server process survived two bridge outages (including the #141 wedge) and three Unity restarts in one session, reconnecting each time, with no loss of tools.
Related: #141 — that issue is the Unity-side cause of the timeouts; this issue is about the client-side reaction to them being fatal.