Skip to content

fix(unity): concurrent, thread-safe, window-independent HTTP server - #8

Open
isekream wants to merge 1 commit into
mainfrom
fix/server-threading-and-headless-startup
Open

fix(unity): concurrent, thread-safe, window-independent HTTP server#8
isekream wants to merge 1 commit into
mainfrom
fix/server-threading-and-headless-startup

Conversation

@isekream

@isekream isekream commented Jun 8, 2026

Copy link
Copy Markdown
Owner

Problem

The editor MCP server accepted connections but frequently returned empty bodies or timed out, and got worse under back-to-back calls. Three coupled defects:

  1. Serial request handlingHandleRequests ran ProcessRequest inline on the single listener thread, which then blocks in ExecuteTool's wait loop for up to requestTimeout seconds. The next GetContext() isn't reached until the current request finishes, so concurrent bridge calls stall in the accept backlog.

  2. Cross-thread EditorApplication.delayCall += — tool work was dispatched to the main thread by mutating the delayCall static delegate from the background listener thread. Delegate += is a non-atomic read-modify-write; concurrent subscription drops callbacks, so the completion flag never flips → timeout with no body. (Fragile even single-threaded — it's an editor-loop delegate being mutated off-thread.)

  3. Window-coupled registry + static-ctor crash — the tool registry lived on the EditorWindow instance, so a closed window made every call return "MCP Server window is not open." Separately, LoadPreferences() called EditorPrefs.* directly in the static constructor, which Unity forbids (GetInt is not allowed to be called from a ScriptableObject constructor).

Fix

  • Service each accepted request on the ThreadPool so the listener loops immediately.
  • Replace the racy delayCall += dispatch with a thread-safe ConcurrentQueue<Action> drained by one persistent EditorApplication.update pump.
  • Make the tool registry static and initialize it on StartServer() — the server now works headless (window closed).
  • Defer LoadPreferences() out of the static constructor into EditorApplication.delayCall.
  • Wrap cross-thread completion flags in a holder with volatile fields for correct visibility.

Tool bodies still run serially on the main thread (Unity API requirement); only the listener and the per-request wait move off it.

Verification

Against a live editor: scene.list_root_objects returned a full success body in ~76 ms (previously empty/timeout), and the "window not open" failure class is gone.

Single file changed: Unity/Editor/McpUnityServer.cs.

🤖 Generated with Claude Code

…pendent

The editor MCP server had coupled defects causing requests to hang or
return empty bodies:

1. Serial request handling: HandleRequests processed each request inline
   on the listener thread, blocking the accept loop for up to
   requestTimeout seconds, so subsequent requests sat unanswered until
   the current one finished. Each request is now serviced on the
   ThreadPool so GetContext() loops immediately.

2. Cross-thread EditorApplication.delayCall: tool execution was dispatched
   to the main thread via `delayCall +=` from the background listener
   thread. delayCall is a non-thread-safe static delegate; concurrent
   subscription drops callbacks, so the completion flag never flips and
   the request times out with no body. Replaced with a thread-safe
   ConcurrentQueue<Action> drained by a single persistent
   EditorApplication.update pump.

3. Window-coupled registry + static-ctor crash: tools lived on the
   EditorWindow instance, so a closed window made every call fail with
   "window not open"; and LoadPreferences() called EditorPrefs.* directly
   in the static constructor, which Unity forbids ("GetInt is not allowed
   to be called from a ScriptableObject constructor"). The registry is
   now static and initialized on server start; preference loading is
   deferred to EditorApplication.delayCall.

Adds volatile completion state for correct cross-thread visibility.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@isekream

isekream commented Jun 8, 2026

Copy link
Copy Markdown
Owner Author

Superseded by #9, which reconciles this threading/headless rewrite with the static registry work and adds domain-reload survival, scene-building tools, Unity 6 compat, and bridge audit fixes. Safe to close.

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.

2 participants