Skip to content

fix(server): keep the shared HTTP server alive when another editor quits - #1380

Open
KamilDev wants to merge 1 commit into
CoplayDev:betafrom
KamilDev:fix/shared-http-server-survives-editor-quit
Open

fix(server): keep the shared HTTP server alive when another editor quits#1380
KamilDev wants to merge 1 commit into
CoplayDev:betafrom
KamilDev:fix/shared-http-server-survives-editor-quit

Conversation

@KamilDev

@KamilDev KamilDev commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Description

Closing one Unity editor kills the shared HTTP-local MCP server that other still-open editors are connected to.

Repro: open two Unity projects with the package on HTTP Local, let one of them launch the server, connect both, then quit the editor that did not launch it. The server process exits and the surviving editor's bridge disconnects (the MCP client loses every instance too).

Root cause (v10.2.0): on EditorApplication.quitting, every editor calls StopManagedLocalHttpServer(), which resolves the server through the launch handshake (pidfile path + instance token). PidFileManager kept that handshake in a single EditorPrefs slot. EditorPrefs is per user (registry / plist), so it is shared by every editor the user has open. The quitting editor therefore read the launcher's handshake, validated the real token against the live process command line, and terminated the shared server. The comment in McpEditorShutdownCleanup saying the stop "never touches servers launched by other Unity instances" was false for instances on the same machine. Same single slot for StoreTracking, so two Unity-managed servers on different ports also overwrote each other's state.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)

Changes Made

Per-project, per-port state instead of one global slot

  • PidFileManager suffixes every handshake/tracking key with .{projectHash}.{port}. Two editors, or two servers on different ports, never read or clear each other's state. StoreHandshake/TryGetHandshake/GetStoredArgsHash/ClearTracking now take the port; the redundant stored-port key and the now-unused TryGetPortFromPidFilePath are gone.

Per-editor-process ownership

  • StoreHandshake also records the launched port in SessionState (MCPForUnity.LocalHttpServer.LaunchedPort): survives domain reloads, dies with the editor process. StopManagedLocalHttpServer() resolves the server to stop from that marker only, so servers launched by another editor, by an earlier run of this editor, or externally (manual uvx) are never resolved at quit. The EditorPrefs handshake still survives restarts so the manual Stop Server button and the pre-launch stop keep their deterministic, token-validated path.

Last-one-out on quit

  • Before stopping, the quit handler asks the server GET /api/instances (already served in local mode for the CLI) and counts instances whose project hash is not ours (our own session may still be listed while the hub processes the WebSocket close we just issued). Other instances connected → leave the server running and log at debug. Probe fails or exceeds its 500 ms budget → also leave it running; a stray headless process is recoverable, a torn-down shared server is not. The quit handler stays bounded: 750 ms transport stop + 500 ms probe.
  • Decision logic is split out as McpEditorShutdownCleanup.ShouldStopManagedServer(int? otherConnectedInstances) and ServerManagementService.TryCountOtherInstances(json, ownHash, out count), mirroring the existing ShouldRunCleanup split.

Unchanged on purpose

  • UNITY_MCP_ALLOW_BATCH batch-mode guard.
  • Fast-quit-after-start fallback and the stale-pidfile / PID-reuse guards in StopLocalHttpServerInternal (they now key off the port they are already operating on). The Python server writes the pidfile before uvicorn binds, so a quit before the port is bound already found nothing to kill; the probe failing in that window changes nothing.
  • EditorPrefsWindow no longer lists the six server keys: they are dynamic per project/port now.

Compatibility / Package Source

  • Unity version(s) tested: Roslyn compile check (tools/compile-check.sh) against 6000.3.14f1 reference assemblies, package + EditMode test sources. No #if UNITY_* gates touched.
  • Package source used: file: (local checkout)
  • Resolved commit hash: n/a (file: source)

Testing/Screenshots/Recordings

  • Python tests: not applicable, no Server/ change
  • Unity EditMode tests (see notes below)
  • Unity PlayMode tests: not applicable
  • Package import/compile check

New/updated EditMode tests:

  • PidFileManagerTests: per-port handshake isolation, two-port slots, launch-marker set by StoreHandshake, cleared by ClearTracking for the same port only, TryGetLaunchedPort false when nothing was launched (the regression case). The fixture saves and restores the host editor's SessionState marker so running the suite never drops a real launch.
  • McpEditorShutdownCleanupTests: ShouldStopManagedServer for 0 / N / probe-failed.
  • ServerManagementServiceInstanceProbeTests: /api/instances parsing — own session filtered (case-insensitive), other project counted, missing hash counted as other, success:false / missing array / malformed / null → false.

What was actually verified, and how

  • Roslyn compile of MCPForUnity.Runtime, MCPForUnity.Editor, TestAsmdef and the EditMode test assembly against 6000.3.14f1 reference assemblies: zero errors. The EditMode tests have not been executed as an NUnit fixture (needs a licensed Editor run; CI's fork-PR EditMode job is a skip). Their assertion bodies were executed against the live Editor via execute_code, see below.
  • Live, two editors on one Unity-managed HTTP-local server (6000.3.14f1, Windows 11):
    • Editor A: project on this branch via file: package. It launched the server (pidfile in its Library/MCPForUnity/RunState, token on the process command line). PackageInfo.FindForAssembly(...).resolvedPath confirmed the patched package was the one loaded.
    • Editor B: unrelated project on the registry release 10.2.0, connected to the same server.
    • In A: TryGetLaunchedPort → 8081, SessionState marker 8081, per-project+port handshake present, old global key absent, TryCountOtherConnectedUnityInstances(8081, 500) → ok, 1 other, 3 ms; against a dead port with a 300 ms budget → false in 312 ms. TryCountOtherInstances on the live /api/instances payload → 1 other from either project's point of view; ShouldStopManagedServer 1 / 0 / null → keep / stop / keep, NUnit asserts passing.
    • Then quit A (the launcher, on the fixed code). Server process still alive and listening on 8081 afterwards; B still listed as the only connected instance; server log shows A's session disconnect followed by the quit handler's GET /api/instances.
  • Not exercised live: the non-launcher quitting on the fixed code (covered by the marker being absent, TryGetLaunchedPort_NothingLaunched_ReturnsFalse), and the genuinely-last-editor stop path (covered by ShouldStopManagedServer(0) plus the unchanged StopLocalHttpServerInternal).

Documentation Updates

  • I have added/removed/modified tools or resources — no; no Python tool/resource/schema change, so website/docs/reference needed no regeneration.

Related Issues

Relates to #1023 (multi-editor support). The reported behaviour is the shutdown half of running several editors against one HTTP-local server.

Additional Notes

  • Stale global keys (MCPForUnity.LocalHttpServer.Last*) from earlier versions are left in place; nothing reads them anymore.
  • Known limitation kept out of scope: only the launching editor process ever stops the server. If the launcher quits first while another editor is connected (server stays up, by design) and that other editor later quits, nobody stops the server, because that editor did not launch it. Ownership hand-over to a surviving editor would be the follow-up.
  • Windows/macOS/Linux behaviour is identical; nothing platform-specific was added.

Summary by CodeRabbit

  • Bug Fixes

    • Improved local server shutdown behavior so a shared server remains running while other Unity instances are connected.
    • Added safer handling when the server cannot be reached during shutdown.
    • Isolated local server tracking by project and port to prevent state conflicts.
  • Tests

    • Expanded coverage for shared-server detection, shutdown decisions, port isolation, and malformed or incomplete server responses.

Two editors connected to one Unity-managed HTTP-local server: closing either one, including the one that never launched it, terminated the server and disconnected the other editor and the MCP client. The launch handshake (pidfile path + instance token) lived in a single EditorPrefs slot, which is per user and shared by every running editor, so a quitting editor read the launcher's handshake, validated the real token, and stopped the server.

- Key the handshake and PID tracking in EditorPrefs per project hash and per port, so concurrent editors and concurrent servers on different ports never read or clear each other's state
- Record the launched port in SessionState as the per-editor-process launch marker; quit cleanup resolves the server to stop from that marker only, so servers launched by other editors or externally are never touched
- Before stopping on quit, ask the server (GET /api/instances) how many other Unity instances are still connected and leave it running unless this editor is the last one; a probe that fails or times out (500 ms) also leaves it running
- Drop the unused TryGetPortFromPidFilePath and the redundant per-slot port key
- Rewrite the misleading "never touches servers launched by other Unity instances" comments
- EditMode tests: per-port isolation and launch-marker lifecycle in PidFileManagerTests, last-one-out decision in McpEditorShutdownCleanupTests, /api/instances response parsing in ServerManagementServiceInstanceProbeTests
@coderabbitai

coderabbitai Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 44b3211c-0e3b-4a30-a0af-61716cfb11b0

📥 Commits

Reviewing files that changed from the base of the PR and between acf5e3d and 62e8ef9.

📒 Files selected for processing (10)
  • MCPForUnity/Editor/Constants/EditorPrefKeys.cs
  • MCPForUnity/Editor/Services/IServerManagementService.cs
  • MCPForUnity/Editor/Services/McpEditorShutdownCleanup.cs
  • MCPForUnity/Editor/Services/Server/IPidFileManager.cs
  • MCPForUnity/Editor/Services/Server/PidFileManager.cs
  • MCPForUnity/Editor/Services/ServerManagementService.cs
  • MCPForUnity/Editor/Windows/EditorPrefs/EditorPrefsWindow.cs
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/McpEditorShutdownCleanupTests.cs
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/Server/PidFileManagerTests.cs
  • TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ServerManagementServiceInstanceProbeTests.cs
💤 Files with no reviewable changes (1)
  • MCPForUnity/Editor/Windows/EditorPrefs/EditorPrefsWindow.cs

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

Changes

The local HTTP server lifecycle now uses project-and-port-scoped tracking. The launching editor records its port in SessionState. Shutdown probes /api/instances and stops the server only when no other Unity instances remain. Tests cover tracking isolation, probe parsing, and shutdown decisions.

Local server tracking

Layer / File(s) Summary
Per-port tracking state
MCPForUnity/Editor/Constants/EditorPrefKeys.cs, MCPForUnity/Editor/Services/Server/*, TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/Server/PidFileManagerTests.cs
Tracking keys now include the project hash and port. The launch marker uses SessionState. Handshake and cleanup APIs now require a port.
Instance probe integration
MCPForUnity/Editor/Services/IServerManagementService.cs, MCPForUnity/Editor/Services/ServerManagementService.cs, MCPForUnity/Editor/Windows/EditorPrefs/EditorPrefsWindow.cs, TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ServerManagementServiceInstanceProbeTests.cs
The service resolves the launched port and queries /api/instances. Parsing counts instances from other project hashes and rejects invalid responses. Obsolete preference mappings are removed.
Last-one-out shutdown
MCPForUnity/Editor/Services/McpEditorShutdownCleanup.cs, TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/McpEditorShutdownCleanupTests.cs
Shutdown uses a bounded probe. The server stops only when the probe reports zero other instances. Probe failure leaves the server running.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: ⚪ Minimal · up to 62e8e

The per-port tracking and shutdown changes preserve shared-server availability for connected editors without any remaining concrete merge-blocking risk.

Sequence Diagram(s)

sequenceDiagram
  participant UnityEditor
  participant McpEditorShutdownCleanup
  participant ServerManagementService
  participant LocalHTTPServer
  UnityEditor->>McpEditorShutdownCleanup: editor shutdown
  McpEditorShutdownCleanup->>ServerManagementService: get launched port
  ServerManagementService-->>McpEditorShutdownCleanup: launched port
  McpEditorShutdownCleanup->>ServerManagementService: count other instances
  ServerManagementService->>LocalHTTPServer: GET /api/instances
  LocalHTTPServer-->>ServerManagementService: instance list
  ServerManagementService-->>McpEditorShutdownCleanup: count or probe failure
  McpEditorShutdownCleanup->>ServerManagementService: stop server when count is zero
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.62% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 78 functions across 9 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary fix: keeping the shared HTTP server running when another Unity editor quits.
Description check ✅ Passed The description follows the repository template and provides the root cause, implementation details, compatibility information, testing status, related issue, and known limitation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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