Skip to content

fix: deliver WebSocket messages while Editor is unfocused (update-drained queue instead of delayCall)#151

Open
Feetschaa wants to merge 1 commit into
CoderGamester:mainfrom
Feetschaa:fix/background-thread-delaycall-dispatch
Open

fix: deliver WebSocket messages while Editor is unfocused (update-drained queue instead of delayCall)#151
Feetschaa wants to merge 1 commit into
CoderGamester:mainfrom
Feetschaa:fix/background-thread-delaycall-dispatch

Conversation

@Feetschaa

@Feetschaa Feetschaa commented Jul 7, 2026

Copy link
Copy Markdown

Problem

MCP tool calls time out whenever the Unity Editor is not the foreground application, and start working again the moment the user clicks back into Unity. This is a common failure mode when driving Unity from an AI client (Claude Code, Cursor, …) running in another window.

Root cause

McpUnitySocketHandler.OnMessage marshals every request onto the Unity main thread with:

EditorApplication.delayCall += () => HandleMessageAsync(data);

OnMessage runs on the WebSocketSharp background thread. EditorApplication.delayCall is a plain static multicast delegate, so this += is a cross-thread read-modify-write, and — more importantly — the main thread's draining of delayCall does not reliably observe a handler added from another thread while the Editor is idle in the background. When Unity has focus there is enough main-thread churn that it eventually fires; unfocused, the queued handler is never invoked and the client times out.

How it was confirmed

  • A heartbeat logged from EditorApplication.update proves update keeps firing while the Editor is backgrounded (~1 Hz on macOS with App Nap disabled).
  • A direct WebSocket probe to ws://localhost:8090/McpUnity while unfocused received no response with the delayCall dispatch, and an immediate response once dispatch was switched to a queue drained from EditorApplication.update.

Fix

Add McpMainThreadDispatcher — a ConcurrentQueue<Action> drained from EditorApplication.update — and enqueue the handler into it instead of using delayCall. No cross-thread delegate mutation; requests are delivered regardless of Editor focus. Behavior when focused is unchanged.

Notes / scope

  • Minimal change, isolated to McpUnitySocketHandler.cs (one new internal helper + the one dispatch line). The server-start path still uses delayCall/update as before.
  • For delivery while fully backgrounded on macOS, the editor loop must keep ticking at all. Disabling App Nap for the Editor process helps and is complementary to this fix: defaults write com.unity3d.UnityEditor5.x NSAppSleepDisabled -bool YES (bundle id varies by Unity major version).

Testing

  • Unity 6000.5.2f1, macOS (Apple Silicon), package v1.3.0.
  • Before: get_play_mode_status and all other tools time out while another app is focused.
  • After: same calls return in < 400 ms with Unity unfocused; focused behavior unchanged.

Relationship to #147 and #150

Addresses #147 ("Unity is required to be in focus").

This is complementary to, and different from, #150:

  • fix: keep editor loop ticking while unfocused on Windows (fixes #147) #150 keeps the editor's tick loop running while backgrounded (Windows-only, native SetTimerQueuePlayerLoopUpdate() every 100 ms). It addresses the loop being parked.
  • This PR changes the dispatch mechanism itself: it stops relying on a cross-thread EditorApplication.delayCall += and instead drains a thread-safe queue from EditorApplication.update. Platform-independent, no native code.

They stack. Importantly, on macOS keeping the loop ticking alone was not sufficient in my testing: with EditorApplication.QueuePlayerLoopUpdate() pumped every editor tick (i.e. the loop demonstrably ticking, update firing at ~1 Hz in the background), delayCall-dispatched requests still never ran and timed out — they only started working after switching dispatch to the update-drained queue in this PR. So this change is needed beyond simply un-parking the loop, and it also removes a latent cross-thread-delegate race on every platform.

For full background delivery on macOS the editor process must also not be suspended by App Nap (defaults write com.unity3d.UnityEditor5.x NSAppSleepDisabled -bool YES), as noted above.

…delayCall

McpUnitySocketHandler.OnMessage marshalled each request onto the main thread
with `EditorApplication.delayCall += ...` from the WebSocketSharp background
thread. delayCall is a plain static delegate; a `+=` from a foreign thread is
not reliably observed or drained by the main thread while the Editor is idle in
the background. As a result, requests that arrived while Unity was not the
foreground application were never executed and the MCP client timed out (a
common complaint when driving Unity from an AI client in another window).

Add McpMainThreadDispatcher: a ConcurrentQueue<Action> drained from
EditorApplication.update (which keeps firing in the background), and enqueue the
handler into it instead of using delayCall. No cross-thread delegate mutation,
so requests are delivered regardless of Editor focus.

Note: for delivery while fully backgrounded on macOS, the editor loop must keep
ticking - disabling App Nap for the Editor process helps
(`defaults write com.unity3d.UnityEditor5.x NSAppSleepDisabled -bool YES`).
apantin added a commit to secondmapstudio/mcp-unity that referenced this pull request Jul 20, 2026
Track the temporary fork + embedded install procedure (PR CoderGamester#151 unfocused fix).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant