Skip to content

Conversation

@dsarno
Copy link
Collaborator

@dsarno dsarno commented Jan 21, 2026

Summary

This PR addresses issue #577 with comprehensive performance optimizations and stability improvements across both the Unity Editor integration and Python server.

Related Work

This PR complements #579, which addressed the stdio transport side of issue #577. This PR focuses on the HTTP transport path and broader editor performance optimizations that benefit all transport modes.

Changes

🚀 Performance Optimizations

1. Eliminated Per-Frame GC Allocations in EditorStateCache

Impact: Reduces 28ms GC spikes during editor updates

  • Implemented early state-change detection using primitive value comparisons before
    expensive JSON snapshot builds
  • Added snapshot caching with DeepClone() result reuse to avoid repeated allocations
  • Reduced update frequency from 250ms to 1000ms (still responsive, lower overhead)
  • Integrated upstream fix for Play mode isCompiling false positives (fix: Filter EditorApplication.isCompiling false positives in Play mode #582)

Files: MCPForUnity/Editor/Services/EditorStateCache.cs

2. Fixed Multiple Domain Reload Loops

Impact: Single domain reload instead of 6-7 when refresh_unity triggers compilation

  • Added retry_on_reload=False parameter to prevent retrying when Unity disconnects during
    compilation
  • Added max_attempts parameter to send_command() to control connection-level retry
    behavior
  • Enhanced connection loss recovery to distinguish expected domain reload disconnects from
    real errors
  • Added "aborted" error pattern for Windows WinError 10053 handling

Files:

  • Server/src/services/tools/refresh_unity.py
  • Server/src/transport/legacy/unity_connection.py

🐛 Bug Fixes

3. UI "Resuming..." Stuck State

Impact: Prevents UI from getting stuck with disabled "Start Session" button

  • Clear ResumeStdioAfterReload and ResumeHttpAfterReload EditorPrefs flags when manually
    ending sessions
  • Prevents transient reload state from persisting after user-initiated disconnection

Files: MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs

4. Windows Log Rotation Errors

Impact: Eliminates noisy PermissionError stack traces on Windows

  • Created WindowsSafeRotatingFileHandler that catches PermissionError during log rotation
  • Windows file locking prevents rotation when another process has log file open (expected
    behavior)

Files: Server/src/main.py

5. Removed Unsupported --reinstall Flag

Impact: Eliminates uvx warning: "Tools cannot be reinstalled via uvx"

  • Removed --reinstall from all uvx command builders (not supported by uvx)
  • Use --no-cache --refresh for dev mode cache busting instead

Files:

  • MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
  • MCPForUnity/Editor/Helpers/AssetPathUtility.cs
  • MCPForUnity/Editor/Helpers/CodexConfigHelper.cs
  • MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs
  • MCPForUnity/Editor/Services/ServerManagementService.cs

6. Python Packaging Fix

Impact: HTTP server can now start correctly

  • Added py-modules = ["main"] to pyproject.toml
  • setuptools.packages.find only discovers packages (directories with __init__.py), must
    explicitly list standalone modules

Files: Server/pyproject.toml

📝 Documentation & Observability

7. Enhanced Connection Loss Handling Documentation

Impact: Clarifies why connection loss during compile is treated as success (addresses PR
feedback)

  • Added detailed comments explaining expected domain reload behavior vs real errors
  • Added info logging when expected disconnect is detected
  • Added warning logging for non-recoverable errors

Files: Server/src/services/tools/refresh_unity.py

8. Fixed Missing Logger Import

Impact: Prevents NameError at runtime in connection loss paths

  • Added missing import logging and logger = logging.getLogger(__name__)

Files: Server/src/services/tools/refresh_unity.py

Testing

  • Pytest: 114/114 passed
  • EditMode Tests: 212/262 completed (50 timed out due to unrelated test
    infrastructure issues)
  • Manual Testing: Verified single domain reload, UI state transitions, HTTP/stdio
    transport switching

Breaking Changes

None.

Migration Notes

None required. All changes are backward compatible.


Commit History:

  1. 51ec4c6 fix: reduce per-frame GC allocations causing editor hitches (issue High performance impact even when MCP server is off #577)
  2. 802072b fix: prevent multiple domain reloads when calling refresh_unity (issue High performance impact even when MCP server is off #577)
  3. dd2132a fix: UI and server stability improvements
  4. 8f5de43 docs: improve refresh_unity connection loss handling documentation
  5. d3a181b fix: add missing logger import in refresh_unity

Summary by Sourcery

Optimize Unity editor integration and Python backend behavior for the Claude Code MCP workflow, reducing GC/connection overhead and improving reliability of CLI-based registration and Unity refresh flows.

New Features:

  • Add asynchronous CLI-based configuration path for Claude Code using thread-safe, pre-captured values and dedicated Claude CLI configurator.
  • Support legacy UnityMCP naming variants and package-version detection when validating or re-registering MCP servers via the Claude CLI.

Bug Fixes:

  • Fix UI sessions getting stuck in a perpetual "Resuming..." state by clearing resume flags when sessions end or orphaned sessions are cleaned up.
  • Resolve Windows log rotation PermissionError crashes by introducing a Windows-safe rotating file handler for the server log.
  • Ensure Python HTTP server packaging exposes the main module correctly so the server can start via setuptools entry points.
  • Prevent domain reload loops and unnecessary retries during Unity compilation by disabling reload retries for refresh_unity and tightening connection-loss handling.

Enhancements:

  • Reduce editor GC pressure and main-thread stalls by throttling state updates, avoiding unnecessary snapshot builds/clones, and short-circuiting empty command queues and editor window polling.
  • Improve Unity refresh readiness detection and connection-loss recovery, distinguishing expected domain reload disconnects from real errors and refining readiness wait conditions.
  • Simplify uvx usage by removing the unsupported --reinstall flag across configuration helpers and documenting the recommended --no-cache --refresh behavior.
  • Enhance migration behavior to auto-reregister CLI-based MCP clients when stdio package versions change instead of blindly reconfiguring JSON-based clients only.
  • Clarify manual configuration snippets and logging/messages around Claude CLI registration, unregistration, and transport/package mismatches.

Build:

  • Adjust Python packaging configuration to use src layout, declare package-dir, and register the standalone main module for distribution.

Documentation:

  • Document expected connection loss behavior during Unity domain reloads and the corresponding logging for recoverable vs non-recoverable refresh_unity failures.

Summary by CodeRabbit

  • New Features

    • Async Claude CLI configuration with background-safe registration, customizable status messages, and CLI-based registration flow.
    • New retry controls for Unity command sends to tune retry-on-reload behavior.
  • Bug Fixes

    • Cleared lingering "Resuming..." UI state when ending sessions.
    • Improved recovery from connection losses during refresh/reload.
    • Log rollover resilient to file-lock errors on Windows.
  • Performance

    • Reduced editor update frequency and added pre-checks to avoid unnecessary snapshots.
    • Avoided per-frame allocations for empty command queues.
  • Improvements

    • Transport/version mismatch detection with conditional auto-rewrite.
    • Packaging metadata added for server distribution.

✏️ Tip: You can customize this high-level summary in your review settings.

dsarno and others added 5 commits January 20, 2026 14:10
…playDev#577)

Eliminate memory allocations that occurred every frame, which triggered
garbage collection spikes (~28ms) approximately every second.

Changes:
- EditorStateCache: Skip BuildSnapshot() entirely when state unchanged
  (check BEFORE building). Increased poll interval from 0.25s to 1.0s.
  Cache DeepClone() results to avoid allocations on GetSnapshot().

- TransportCommandDispatcher: Early exit before lock/list allocation
  when Pending.Count == 0, eliminating per-frame allocations when idle.

- StdioBridgeHost: Same early exit pattern for commandQueue.

- MCPForUnityEditorWindow: Throttle OnEditorUpdate to 2-second intervals
  instead of every frame, preventing expensive socket checks 60+/sec.

Fixes GitHub issue CoplayDev#577: High performance impact even when MCP server is off
…CoplayDev#577)

Root Cause:
- send_command() had a hardcoded retry loop (min 6 attempts) on connection errors
- Each retry resent the refresh_unity command, causing Unity to reload 6 times
- retry_on_reload=False only controlled reload-state retries, not connection retries

The Fix:
1. Unity C# (MCPForUnity/Editor):
   - Added --reinstall flag to uvx commands in dev mode
   - Ensures local development changes are picked up by uvx/Claude Code
   - Applies to all client configurators (Claude Code, Codex, etc.)

2. Python Server (Server/src):
   - Added max_attempts parameter to send_command()
   - Pass max_attempts=0 when retry_on_reload=False
   - Fixed type handling in refresh_unity.py (handle MCPResponse objects)
   - Added timeout to connection error recovery conditions
   - Recovery logic now returns success instead of error to prevent client retries

Changes:
- MCPForUnity/Editor: Added --reinstall to dev mode uvx commands
- Server/refresh_unity.py: Fixed type handling, improved error recovery
- Server/unity_connection.py: Added max_attempts param, disable retries when retry_on_reload=False

Result: refresh_unity with compile=request now triggers only 1 domain reload instead of 6

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Unity Editor (C#):
- Fix "Resuming..." stuck state when manually clicking End Session
  Clear ResumeStdioAfterReload and ResumeHttpAfterReload flags in
  OnConnectionToggleClicked and EndOrphanedSessionAsync to prevent
  UI from getting stuck showing "Resuming..." with disabled button
- Remove unsupported --reinstall flag from all uvx command builders
  uvx does not support --reinstall and shows warning when used
  Use --no-cache --refresh instead for dev mode cache busting

Python Server:
- Add "aborted" to connection error patterns in refresh_unity
  Handle WinError 10053 (connection aborted) gracefully during
  Unity domain reload, treating it as expected behavior
- Add WindowsSafeRotatingFileHandler to suppress log rotation errors
  Windows file locking prevents log rotation when file is open by
  another process; catch PermissionError to avoid noisy stack traces
- Fix packaging: add py-modules = ["main"] to pyproject.toml
  setuptools.packages.find only discovers packages (directories with
  __init__.py), must explicitly list standalone module files

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Add detailed comments and logging to clarify why connection loss during
compile is treated as success (expected domain reload behavior, not failure).
This addresses PR feedback about potentially masking real connection errors.

The logic is intentional and correct:
- Connection loss only treated as success when compile='request'
- Domain reload causing disconnect is expected Unity behavior
- Subsequent wait_for_ready loop validates Unity becomes ready
- Prevents multiple domain reload loops (issue CoplayDev#577)

Added logging for observability:
- Info log when expected disconnect detected
- Warning log for non-recoverable errors

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Missing logger import causes NameError at runtime when connection
loss handling paths are triggered (lines 82 and 91).

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Jan 21, 2026

Reviewer's Guide

This PR focuses on eliminating Unity editor GC hitches and domain reload loops while improving Claude Code CLI integration, HTTP/stdio transport consistency, and server stability, including packaging and logging fixes.

Sequence diagram for async Claude CLI configuration from Unity editor

sequenceDiagram
    actor User
    participant UnityWindow as MCPForUnityEditorWindow
    participant ConfigSection as McpClientConfigSection
    participant Configurator as ClaudeCodeConfigurator
    participant CliConfigurator as ClaudeCliMcpConfigurator
    participant TaskRunner as Task_Run
    participant ClaudeCLI as Claude_CLI_Process

    User->>UnityWindow: Click Configure button
    UnityWindow->>ConfigSection: OnConfigureClicked()

    ConfigSection->>Configurator: get Status
    alt Configurator is CLI-based
        ConfigSection->>ConfigSection: ConfigureClaudeCliAsync(Configurator)
        ConfigSection->>ConfigSection: statusRefreshInFlight.Add(Configurator)
        ConfigSection->>ConfigSection: ApplyStatusToUi(showChecking=true, customMessage="Registering..." or "Unregistering...")

        ConfigSection->>ConfigSection: Capture projectDir, useHttpTransport, claudePath, httpUrl, uvxPath, gitUrl, packageName, shouldForceRefresh, pathPrepend

        ConfigSection->>TaskRunner: Task.Run(lambda ...)
        activate TaskRunner
        TaskRunner->>CliConfigurator: ConfigureWithCapturedValues(projectDir, claudePath, pathPrepend, useHttpTransport, httpUrl, uvxPath, gitUrl, packageName, shouldForceRefresh)

        alt client.status == Configured
            CliConfigurator->>CliConfigurator: UnregisterWithCapturedValues(...)
            CliConfigurator->>ClaudeCLI: claude mcp remove UnityMCP
            CliConfigurator->>ClaudeCLI: claude mcp remove unityMCP
            CliConfigurator->>CliConfigurator: client.SetStatus(NotConfigured)
        else Register
            CliConfigurator->>CliConfigurator: RegisterWithCapturedValues(...)
            CliConfigurator->>ClaudeCLI: claude mcp remove UnityMCP
            CliConfigurator->>ClaudeCLI: claude mcp remove unityMCP
            CliConfigurator->>ClaudeCLI: claude mcp add --transport http|stdio UnityMCP ...
            alt CLI success
                CliConfigurator->>CliConfigurator: client.SetStatus(Configured)
            else CLI failure
                CliConfigurator-->>TaskRunner: throw InvalidOperationException
            end
        end

        TaskRunner-->>ConfigSection: ContinueWith(result or error)
        deactivate TaskRunner

        ConfigSection->>ConfigSection: statusRefreshInFlight.Remove(Configurator)
        ConfigSection->>ConfigSection: lastStatusChecks.Remove(Configurator)

        alt errorMessage != null
            ConfigSection->>Configurator: Client.SetStatus(Error, errorMessage)
            ConfigSection->>ConfigSection: RefreshClientStatus(forceImmediate=true)
        else success
            ConfigSection->>ConfigSection: lastStatusChecks[Configurator] = DateTime.UtcNow
            ConfigSection->>ConfigSection: ApplyStatusToUi(showChecking=false)
        end
        ConfigSection->>ConfigSection: UpdateManualConfiguration()
    else Non-CLI configurator
        ConfigSection->>ConfigSection: synchronous ConfigureClient
    end
Loading

Sequence diagram for fixing stuck "Resuming..." state on session end

sequenceDiagram
    actor User
    participant ConnSection as McpConnectionSection
    participant Bridge as BridgeService
    participant EditorPrefs as EditorPrefs

    User->>ConnSection: Click End Session toggle
    ConnSection->>ConnSection: OnConnectionToggleClicked()

    alt bridgeService.IsRunning is True
        ConnSection->>EditorPrefs: DeleteKey(ResumeStdioAfterReload)
        ConnSection->>EditorPrefs: DeleteKey(ResumeHttpAfterReload)
        ConnSection->>Bridge: StopAsync()
        Bridge-->>ConnSection: stopped
    else starting or not running
        ConnSection->>ConnSection: normal start logic
    end

    ConnSection->>ConnSection: Update UI (Start Session enabled, no "Resuming..." text)

    Note over ConnSection,EditorPrefs: EndOrphanedSessionAsync() also clears resume flags before StopAsync() to avoid stuck "Resuming..." state
Loading

Class diagram for updated MCP configurators and Claude Code CLI integration

classDiagram
    class IMcpClientConfigurator {
        <<interface>>
        +McpClient Client
        +McpStatus Status
        +bool SupportsAutoConfigure
        +void Configure()
        +void CheckStatus(bool attemptAutoRewrite)
        +string GetConfigureActionLabel()
    }

    class McpClientConfiguratorBase {
        +McpClient Client
        +McpStatus status
        +bool SupportsAutoConfigure
        +McpStatus CheckStatusWithProjectDir(string projectDir, bool useHttpTransport, bool attemptAutoRewrite)
        +void Configure()
        +void ConfigureWithCapturedValues(string projectDir, string claudePath, string pathPrepend, bool useHttpTransport, string httpUrl, string uvxPath, string gitUrl, string packageName, bool shouldForceRefresh)
        +void Register()
        +void RegisterWithCapturedValues(string projectDir, string claudePath, string pathPrepend, bool useHttpTransport, string httpUrl, string uvxPath, string gitUrl, string packageName, bool shouldForceRefresh)
        +void Unregister()
        +void UnregisterWithCapturedValues(string projectDir, string claudePath, string pathPrepend)
        +string GetManualSnippet()
        +IList~string~ GetInstallationSteps()
        +static string ExtractPackageSourceFromCliOutput(string cliOutput)
    }

    class ClaudeCliMcpConfigurator {
        +bool SupportsHttpTransport
        +bool SupportsAutoConfigure
        +void ConfigureWithCapturedValues(string projectDir, string claudePath, string pathPrepend, bool useHttpTransport, string httpUrl, string uvxPath, string gitUrl, string packageName, bool shouldForceRefresh)
        +void RegisterWithCapturedValues(string projectDir, string claudePath, string pathPrepend, bool useHttpTransport, string httpUrl, string uvxPath, string gitUrl, string packageName, bool shouldForceRefresh)
        +void UnregisterWithCapturedValues(string projectDir, string claudePath, string pathPrepend)
    }

    class ClaudeCodeConfigurator {
        +ClaudeCodeConfigurator()
        +IList~string~ GetInstallationSteps()
    }

    class JsonFileMcpConfigurator {
        +bool SupportsHttpTransport
        +bool SupportsAutoConfigure
        +void Configure()
        +void CheckStatus(bool attemptAutoRewrite)
        +string GetManualSnippet()
    }

    class McpClient {
        +string name
        +bool SupportsHttpTransport
    }

    class StdIoVersionMigration {
        +static void RunMigrationIfNeeded()
        -static bool ConfigUsesStdIo(McpClient client)
    }

    IMcpClientConfigurator <|.. McpClientConfiguratorBase
    McpClientConfiguratorBase <|-- ClaudeCliMcpConfigurator
    McpClientConfiguratorBase <|-- JsonFileMcpConfigurator
    ClaudeCliMcpConfigurator <|-- ClaudeCodeConfigurator

    McpClientConfiguratorBase o-- McpClient
    ClaudeCodeConfigurator o-- McpClient

    StdIoVersionMigration ..> IMcpClientConfigurator : uses
    StdIoVersionMigration ..> ClaudeCliMcpConfigurator : checks and auto-rewrites

    class McpClientConfigSection {
        -List~IMcpClientConfigurator~ configurators
        -Dictionary~IMcpClientConfigurator, DateTime~ lastStatusChecks
        -HashSet~IMcpClientConfigurator~ statusRefreshInFlight
        -int selectedClientIndex
        +void OnConfigureClicked()
        -void ConfigureClaudeCliAsync(IMcpClientConfigurator client)
        -void ApplyStatusToUi(IMcpClientConfigurator client, bool showChecking, string customMessage)
        -void RefreshClientStatus(IMcpClientConfigurator client, bool forceImmediate)
    }

    McpClientConfigSection ..> IMcpClientConfigurator : configures
    McpClientConfigSection ..> ClaudeCliMcpConfigurator : async configure
    McpClientConfigSection ..> McpClientConfiguratorBase : updates Client status
Loading

Class diagram for optimized EditorStateCache and transport processing

classDiagram
    class EditorStateCache {
        <<static>>
        -JObject _cached
        -long _sequence
        -long? _domainReloadAfterUnixMs
        -double _lastUpdateTimeSinceStartup
        -const double MinUpdateIntervalSeconds
        -JObject _cachedClone
        -long _cachedCloneSequence
        -string _lastTrackedScenePath
        -string _lastTrackedSceneName
        -bool _lastTrackedIsFocused
        -bool _lastTrackedIsPlaying
        -bool _lastTrackedIsPaused
        -bool _lastTrackedIsUpdating
        -bool _lastTrackedTestsRunning
        -string _lastTrackedActivityPhase
        -bool _domainReloadPending
        -bool _lastIsCompiling
        +void Initialize()
        +void ForceUpdate(string reason)
        +JObject GetSnapshot()
        -void OnUpdate()
        -static bool GetActualIsCompiling()
    }

    class TransportCommandDispatcher {
        <<static>>
        -Dictionary~string, PendingCommand~ Pending
        -object PendingLock
        -int _processingFlag
        +void Enqueue(string id, PendingCommand command)
        -void UnhookUpdateIfIdle()
        -void ProcessQueue()
    }

    class StdioBridgeHost {
        <<static>>
        -Dictionary~string, QueuedCommand~ commandQueue
        -object lockObj
        -float nextHeartbeatAt
        +void EnqueueCommand(string id, QueuedCommand command)
        -void ProcessCommands()
    }

    class MCPForUnityEditorWindow {
        -double _lastEditorUpdateTime
        -const double EditorUpdateIntervalSeconds
        +void OnEnable()
        +void OnDisable()
        +void OnFocus()
        -void OnEditorUpdate()
    }

    EditorStateCache ..> EditorApplication : uses
    EditorStateCache ..> EditorSceneManager : uses
    EditorStateCache ..> InternalEditorUtility : uses
    EditorStateCache ..> TestRunStatus : uses

    TransportCommandDispatcher ..> PendingCommand : manages
    StdioBridgeHost ..> QueuedCommand : manages

    MCPForUnityEditorWindow ..> TransportCommandDispatcher : polls
    MCPForUnityEditorWindow ..> StdioBridgeHost : polls

    class PendingCommand {
        +string Id
        +object Payload
        +bool IsReady()
    }

    class QueuedCommand {
        +string Id
        +object Payload
        +bool IsReady()
    }
Loading

File-Level Changes

Change Details Files
Optimize editor state polling and command queues to remove per-frame allocations and reduce GC spikes.
  • Increase EditorStateCache update interval from 250ms to 1s and add fast state-change detection before building JSON snapshots.
  • Introduce cached DeepClone() reuse in EditorStateCache.GetSnapshot() keyed by a sequence number.
  • Short-circuit empty TransportCommandDispatcher and StdioBridgeHost queues before taking locks or allocating lists.
  • Throttle MCPForUnityEditorWindow.OnEditorUpdate to run every 2 seconds to avoid frequent expensive connection checks.
MCPForUnity/Editor/Services/EditorStateCache.cs
MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs
MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs
MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
Improve Claude Code CLI-based configuration, including auto-repair of mismatched registrations and async configuration flow.
  • Extend McpClientConfiguratorBase to detect both 'UnityMCP' and legacy 'unityMCP' registrations and compare configured transport and uvx package source.
  • If transport or package source mismatches are detected, either auto-reregister via Configure() or surface clear error/status messages.
  • Add thread-safe ConfigureWithCapturedValues, RegisterWithCapturedValues, and UnregisterWithCapturedValues that operate on pre-captured main-thread values and handle both naming variants when adding/removing registrations.
  • Adjust Register and Unregister to always remove both UnityMCP/unityMCP before registering and simplify unregister flow.
  • Update manual snippet text and comments to remove the project-dir caveat for 'claude mcp list' and document uvx flag behavior.
  • Make ClaudeCodeConfigurator inherit from ClaudeCliMcpConfigurator and update installation steps to describe CLI-based registration instead of JSON editing.
  • In the client config UI, route ClaudeCliMcpConfigurator through an async ConfigureClaudeCliAsync path that captures all needed values on the main thread, runs CLI operations on a background Task, and updates UI/status safely on the main thread with optional custom messages.
MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
MCPForUnity/Editor/Clients/Configurators/ClaudeCodeConfigurator.cs
MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
Fix domain reload behavior and connection handling to avoid multiple reload loops and treat expected disconnects as success.
  • Add a retry_on_reload flag and max_attempts parameter to send_command_with_retry/async_send_command_with_retry and plumb retry_on_reload=False from refresh_unity to avoid resending commands that trigger compilation/reload.
  • Extend UnityConnection.send_command to accept an optional max_attempts to override connection-level retry behavior and wire that from the retry helper.
  • In refresh_unity, normalize responses to dicts, explicitly classify connection-lost scenarios (closed, disconnected, aborted, timeout, reloading) as expected when compile='request', and treat them as success while logging informative messages.
  • Introduce a real_blocking_reasons set in the wait_for_ready loop so that only genuine busy states (compiling, domain_reload, running_tests, asset_refresh) block; stale_status alone no longer blocks indefinitely.
  • Add detailed comments and logging (info for expected disconnect, warning for non-recoverable errors) to clarify behavior and aid debugging.
Server/src/services/tools/refresh_unity.py
Server/src/transport/legacy/unity_connection.py
Fix UI resume-state bugs so sessions don’t get stuck in a 'Resuming...' state after manual disconnects or orphan cleanup.
  • On manual End Session and in EndOrphanedSessionAsync, clear ResumeStdioAfterReload and ResumeHttpAfterReload EditorPrefs keys before stopping the bridge.
  • Keep connection toggle UX responsive by disabling the button only while the async stop is in progress.
MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs
Standardize uvx usage and dev-mode refresh flags while removing unsupported --reinstall usage across the editor.
  • Replace any implicit --reinstall behavior with '--no-cache --refresh' in uvx command construction for stdio transport registration and HTTP dev server runs.
  • Document in comments that --reinstall is not supported by uvx and that ShouldForceUvxRefresh controls use of --no-cache/--refresh.
  • Update manual configuration snippets to reflect the current recommended claude mcp usage and uvx flags.
MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
MCPForUnity/Editor/Helpers/AssetPathUtility.cs
MCPForUnity/Editor/Helpers/CodexConfigHelper.cs
MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs
MCPForUnity/Editor/Services/ServerManagementService.cs
Fix Python packaging and logging issues on the server to improve startup reliability and Windows stability.
  • Configure setuptools in pyproject.toml to discover packages under src and explicitly register main as a py-module so the HTTP server entry point is importable.
  • Introduce WindowsSafeRotatingFileHandler and use it in main.py to wrap RotatingFileHandler, catching PermissionError during rotation to avoid noisy stack traces when files are locked by another process.
Server/pyproject.toml
Server/src/main.py
Adjust stdio version migration to cooperate with CLI-based configurators and auto-fix mismatched registrations.
  • Update StdIoVersionMigration to skip non-auto-configurable clients, use CheckStatus(attemptAutoRewrite: true) for ClaudeCliMcpConfigurator instances, and only run ConfigureClient for JSON-based configurators that actually use stdio.
  • Track whether any configurator status changed as part of the migration to preserve existing behavior.
MCPForUnity/Editor/Migrations/StdIoVersionMigration.cs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 21, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Replaces JSON-file based Claude configurator with a CLI-driven configurator, adds background-safe captured-value registration/unregistration, extends status checks and auto-rewrite logic, and updates UI to run CLI registration asynchronously. Also includes transport, server, and Editor performance/robustness tweaks.

Changes

Cohort / File(s) Summary of changes
CLI configurator core
MCPForUnity/Editor/Clients/Configurators/ClaudeCodeConfigurator.cs, MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
Switched configurator base to ClaudeCliMcpConfigurator, added ConfigureWithCapturedValues / RegisterWithCapturedValues / UnregisterWithCapturedValues, package-source extraction, transport/version checks, multi-name registry handling, and conditional auto-rewrite logic.
Editor UI & async flow
MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs, MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs, MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
Run Claude CLI registration asynchronously with main-thread capture and background execution; show contextual in-progress messages; clean up resume flags; throttle editor updates to reduce per-frame work.
StdIO/version migration
MCPForUnity/Editor/Migrations/StdIoVersionMigration.cs
Split migration paths, call Claude CLI configurator CheckStatus with attemptAutoRewrite=true, and branch JSON/config-file handling separately.
Transport queue & stdio host
MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs, MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs
Add early-exit inside locks when queues are empty to avoid per-frame allocations.
Stability & retry semantics (server)
Server/src/transport/legacy/unity_connection.py, Server/src/main.py
Added max_attempts and retry_on_reload controls to send_command/send_command_with_retry/async_send_command_with_retry; added Windows-safe rotating file handler to ignore PermissionError on rollover.
Server refresh flow
Server/src/services/tools/refresh_unity.py
Normalize MCP responses, add recoverable-disconnect handling, readiness loop tweaks, timeout behavior, and improved logging for recover/retry semantics.
Editor state & caching
MCPForUnity/Editor/Services/EditorStateCache.cs
Increase min update throttle (0.25→1.0s), add cheap pre-build state-change detection to skip snapshot builds when unchanged, refine compilation-edge handling.
Minor docs/args/comments
MCPForUnity/Editor/Helpers/AssetPathUtility.cs, MCPForUnity/Editor/Helpers/CodexConfigHelper.cs, MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs, MCPForUnity/Editor/Services/ServerManagementService.cs
Documentation and comment updates noting uvx does not support --reinstall, recommending --no-cache --refresh; no functional changes.
Packaging metadata
Server/pyproject.toml
Added setuptools packaging metadata ([tool.setuptools.*]) pointing at src.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant EditorUI as "Unity Editor UI"
    participant BG as "Background Task"
    participant ClaudeCLI as "Claude CLI"
    participant Registry as "MCP Registry"

    User->>EditorUI: Click Configure (Claude)
    EditorUI->>EditorUI: Capture main-thread values (projectDir, claudePath, pathPrepend, transport flags, urls, uvx/git/package)
    EditorUI->>BG: Start ConfigureClaudeCliAsync with captured values
    BG->>ClaudeCLI: Invoke ConfigureWithCapturedValues / RegisterWithCapturedValues
    ClaudeCLI->>ClaudeCLI: Inspect existing registration (list/get), extract package source
    ClaudeCLI-->>Registry: Run CLI commands to unregister/register (add/remove MCP entry)
    Registry-->>ClaudeCLI: Return registration status/output
    ClaudeCLI-->>BG: Return success/error and status
    BG->>EditorUI: Update UI state (success or error message)
    EditorUI->>User: Show final status
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • justinpbarnett

Poem

"I hopped through code at dawn's first light,
Captured paths then ran with might.
CLI bells and registry cheer,
Background work — no blocking here.
A rabbit's nibble, quick and bright 🐰"

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title comprehensively summarizes the main changes: performance optimizations, Claude code configuration updates, and stability improvements. It directly aligns with the extensive modifications across editor services, retry logic, UI fixes, logging, CLI/uvx commands, and packaging.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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 and usage tips.

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 4 issues, and left some high level feedback:

  • The new snapshot caching in EditorStateCache.GetSnapshot() now returns a shared JObject instance instead of a fresh DeepClone each time, which changes semantics and allows callers to mutate the cached object; consider either keeping per-call cloning or returning a read-only/deep-cloned view to avoid subtle shared-state bugs.
  • In TransportCommandDispatcher.ProcessQueue and StdioBridgeHost.ProcessCommands you read Pending.Count/commandQueue.Count outside the lock, but these collections are otherwise guarded by a lock; using Count without synchronization can lead to race conditions or undefined behavior, so it would be safer to base the early exit on a separate atomic flag or perform the check inside the lock only.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new snapshot caching in EditorStateCache.GetSnapshot() now returns a shared JObject instance instead of a fresh DeepClone each time, which changes semantics and allows callers to mutate the cached object; consider either keeping per-call cloning or returning a read-only/deep-cloned view to avoid subtle shared-state bugs.
- In TransportCommandDispatcher.ProcessQueue and StdioBridgeHost.ProcessCommands you read Pending.Count/commandQueue.Count outside the lock, but these collections are otherwise guarded by a lock; using Count without synchronization can lead to race conditions or undefined behavior, so it would be safer to base the early exit on a separate atomic flag or perform the check inside the lock only.

## Individual Comments

### Comment 1
<location> `MCPForUnity/Editor/Services/EditorStateCache.cs:508-514` </location>
<code_context>
+                // Cache the cloned version to avoid allocating a new JObject on every GetSnapshot() call.
+                // This fixes the GC spikes that occurred when external tools polled editor state frequently.
+                // Only regenerate the clone when _cached changes (detected via sequence number).
+                if (_cachedClone == null || _cachedCloneSequence != _sequence)
+                {
+                    _cachedClone = (JObject)_cached.DeepClone();
+                    _cachedCloneSequence = _sequence;
+                }
+
+                return _cachedClone;
             }
         }
</code_context>

<issue_to_address>
**issue (bug_risk):** Returning a shared cached JObject can leak mutations to all callers and break expectations from the previous DeepClone() behavior.

This changes the contract of GetSnapshot(): it no longer returns an isolated clone but a shared instance. Any caller that mutates the returned JObject will now inadvertently modify the cached snapshot seen by others, risking hard‑to‑trace bugs.

To keep allocations down without changing semantics, either:
- Keep `_cached` as the mutable source of truth and always return `DeepClone()` (optimize how often `_cached` itself changes), or
- Treat the cached snapshot as immutable and restrict mutations to a separate builder object, swapping references only when ready.

Unless you can ensure all consumers treat the returned JObject as strictly read‑only, I’d avoid sharing the cached clone and preserve the original cloning behavior.
</issue_to_address>

### Comment 2
<location> `Server/src/services/tools/refresh_unity.py:103-104` </location>
<code_context>
         timeout_s = 60.0
         start = time.monotonic()

+        # Blocking reasons that indicate Unity is actually busy (not just stale status)
+        real_blocking_reasons = {"compiling", "domain_reload", "running_tests", "asset_refresh"}
+
         while time.monotonic() - start < timeout_s:
</code_context>

<issue_to_address>
**issue (bug_risk):** The "asset_refresh" blocking reason may not match the activityPhase values produced by the Unity editor state.

In editor_state, the activityPhase set when EditorApplication.isUpdating is true is "asset_import". If blocking_reasons are based on that value (or share its terminology), "asset_refresh" here may never appear in advice["blocking_reasons"], so asset import would be treated as non‑blocking and the loop could exit too early.

Please confirm the exact strings used for editor_state blocking_reasons. If they’re aligned with activityPhase, this constant should likely be "asset_import" instead of "asset_refresh" so they match.
</issue_to_address>

### Comment 3
<location> `MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs:226-228` </location>
<code_context>
+            // This check is safe because Pending.Count is only modified under PendingLock,
+            // and a brief race (seeing 0 when a command was just added) is harmless since
+            // RequestMainThreadPump() will call ProcessQueue() again immediately.
+            if (Pending.Count == 0)
+            {
+                return;
+            }
+
</code_context>

<issue_to_address>
**issue (bug_risk):** Reading Pending.Count without holding PendingLock introduces a data race and potential InvalidOperationExceptions.

Because Pending is a mutable collection protected by PendingLock everywhere else, reading Pending.Count without that lock is unsafe. If another thread mutates the dictionary while this method is running, you can both miss newly enqueued commands and hit InvalidOperationExceptions from concurrent reads/writes.

You can keep the early return while staying thread‑safe by taking the lock for the count check:

```csharp
lock (PendingLock)
{
    if (Pending.Count == 0)
    {
        return;
    }
}
```

This mirrors the later double‑check inside the lock and removes the unsynchronized access.
</issue_to_address>

### Comment 4
<location> `MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs:823-825` </location>
<code_context>

+                // Early exit BEFORE acquiring lock or allocating. This prevents the per-frame
+                // List allocations that trigger GC every ~1 second (GitHub issue #577).
+                if (commandQueue.Count == 0)
+                {
+                    return;
+                }
+
</code_context>

<issue_to_address>
**issue (bug_risk):** commandQueue.Count is checked without taking lockObj, which is inconsistent with the locking discipline around commandQueue.

If commandQueue is a non-thread-safe collection, reading Count without the lock while other operations are locked can cause runtime exceptions or data races under concurrent access.

You can preserve the optimization by performing the early exit inside the existing lock:

```csharp
List<(string id, QueuedCommand command)> work;
lock (lockObj)
{
    if (commandQueue.Count == 0)
    {
        return;
    }
    work = new List<(string, QueuedCommand)>(commandQueue.Count);
    ...
}
```

This keeps all access to commandQueue serialized while still avoiding unnecessary list allocations.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs (1)

394-472: Transport and version mismatch detection logic is well-structured.

The enhanced status checking with support for both "UnityMCP" and legacy "unityMCP" naming, combined with transport mode and package version verification, is a solid improvement. The auto-rewrite behavior with attemptAutoRewrite is properly guarded.

However, there's a potential issue at line 443: calling Configure() from within CheckStatusWithProjectDir can lead to main-thread access violations when CheckStatusWithProjectDir is invoked from a background thread (e.g., via RefreshClaudeCliStatus in McpClientConfigSection.cs).

Analysis of the thread-safety concern

Configure() internally calls Register() which accesses EditorPrefs and Application.dataPath (lines 585, 602), both of which require main-thread execution. If CheckStatusWithProjectDir is called from a background thread with attemptAutoRewrite: true, this will throw.

Current mitigation: In McpClientConfigSection.cs, RefreshClaudeCliStatus passes attemptAutoRewrite: false (line 451), which avoids this path. However, this creates a latent bug if anyone calls CheckStatusWithProjectDir(..., attemptAutoRewrite: true) from a background context.

Consider either:

  1. Documenting that attemptAutoRewrite: true must only be called from the main thread, or
  2. Using a captured-values variant for the auto-rewrite path similar to ConfigureWithCapturedValues.
Server/src/services/tools/refresh_unity.py (1)

103-122: Track readiness to avoid false “ready” after timeout.

If the wait loop times out, recovered_from_disconnect still returns success with “editor is ready”. That can mislead callers and trigger tools while Unity is still busy/unavailable. Please track readiness explicitly and return a timeout failure (or a “not confirmed” flag) when the loop exits without a ready signal.

🛠️ Proposed fix
-        while time.monotonic() - start < timeout_s:
+        ready_confirmed = False
+        while time.monotonic() - start < timeout_s:
             state_resp = await editor_state.get_editor_state(ctx)
             state = state_resp.model_dump() if hasattr(
                 state_resp, "model_dump") else state_resp
             data = (state or {}).get("data") if isinstance(
                 state, dict) else None
             advice = (data or {}).get(
                 "advice") if isinstance(data, dict) else None
             if isinstance(advice, dict):
                 # Exit if ready_for_tools is True
                 if advice.get("ready_for_tools") is True:
+                    ready_confirmed = True
                     break
                 # Also exit if the only blocking reason is "stale_status" (Unity in background)
                 # Staleness means we can't confirm status, not that Unity is actually busy
                 blocking = set(advice.get("blocking_reasons") or [])
                 if not (blocking & real_blocking_reasons):
+                    ready_confirmed = True
                     break
             await asyncio.sleep(0.25)
+
+        if wait_for_ready and not ready_confirmed:
+            return MCPResponse(
+                success=False,
+                message="Timed out waiting for Unity to become ready.",
+                hint="retry",
+            )
🧹 Nitpick comments (5)
Server/src/main.py (1)

42-52: Consider logging the skipped rollover for observability.

The implementation correctly handles Windows file locking by catching PermissionError. However, silently ignoring the exception provides no visibility into repeated rollover failures, which could cause unbounded log growth if the file remains locked.

🔧 Suggested improvement: Add debug logging
 class WindowsSafeRotatingFileHandler(RotatingFileHandler):
     """RotatingFileHandler that gracefully handles Windows file locking during rotation."""

     def doRollover(self):
         """Override to catch PermissionError on Windows when log file is locked."""
         try:
             super().doRollover()
         except PermissionError:
             # On Windows, another process may have the log file open.
             # Skip rotation this time - we'll try again on the next rollover.
-            pass
+            # Log at debug level to aid troubleshooting without cluttering output
+            if self.stream:
+                try:
+                    self.stream.write(
+                        "[WindowsSafeRotatingFileHandler] Rollover skipped due to PermissionError\n"
+                    )
+                except Exception:
+                    pass

Alternatively, you could use logging.getLogger(__name__).debug(...) but that risks recursion if this handler is attached to that logger.

Server/src/transport/legacy/unity_connection.py (1)

236-243: Fix implicit Optional type annotation.

Per PEP 484 and the static analysis hint, the params parameter should use explicit union syntax rather than implicit Optional.

Suggested fix
-    def send_command(self, command_type: str, params: dict[str, Any] = None, max_attempts: int | None = None) -> dict[str, Any]:
+    def send_command(self, command_type: str, params: dict[str, Any] | None = None, max_attempts: int | None = None) -> dict[str, Any]:
MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs (1)

719-759: ExtractPackageSourceFromCliOutput parsing logic is reasonable but could be more robust.

The parsing handles both quoted and unquoted values, which is good. However, there are a few edge cases to consider:

  1. If the CLI output contains --from in a comment or unrelated context, this could produce false matches.
  2. The method doesn't handle escaped quotes within quoted strings.

For the current use case (parsing well-structured CLI output), this is likely sufficient.

💡 Optional: Consider using a regex for clearer intent
private static readonly Regex FromArgRegex = new Regex(
    @"--from\s+(?:""([^""]+)""|'([^']+)'|(\S+))",
    RegexOptions.IgnoreCase | RegexOptions.Compiled);

private static string ExtractPackageSourceFromCliOutput(string cliOutput)
{
    if (string.IsNullOrEmpty(cliOutput))
        return null;

    var match = FromArgRegex.Match(cliOutput);
    if (!match.Success)
        return null;

    return match.Groups[1].Success ? match.Groups[1].Value
         : match.Groups[2].Success ? match.Groups[2].Value
         : match.Groups[3].Value;
}
MCPForUnity/Editor/Services/EditorStateCache.cs (2)

291-340: Effective optimization pattern - early state change detection.

The "cheap checks before expensive operation" pattern is well-implemented. The state comparisons are much cheaper than building a full JSON snapshot.

One minor observation: the string comparison for _lastTrackedScenePath != scenePath uses reference equality. If Unity's scene.path returns a new string instance with the same value, this could trigger unnecessary rebuilds. Consider using string.Equals() for value comparison if spurious updates are observed in practice.

Optional: Use value comparison for strings
-            bool hasChanges = compilationEdge
-                || _lastTrackedScenePath != scenePath
-                || _lastTrackedSceneName != sceneName
+            bool hasChanges = compilationEdge
+                || !string.Equals(_lastTrackedScenePath, scenePath, StringComparison.Ordinal)
+                || !string.Equals(_lastTrackedSceneName, sceneName, StringComparison.Ordinal)

303-323: Consider extracting activity phase computation to a helper method.

The activity phase determination logic is duplicated here and in BuildSnapshot() (lines 389-409). While the duplication is intentional for the optimization pattern, extracting it to a shared helper would improve maintainability if the phase logic evolves.

Optional: Extract to helper method
private static string ComputeActivityPhase(bool isCompiling, bool testsRunning, bool isUpdating)
{
    if (testsRunning) return "running_tests";
    if (isCompiling) return "compiling";
    if (_domainReloadPending) return "domain_reload";
    if (isUpdating) return "asset_import";
    if (EditorApplication.isPlayingOrWillChangePlaymode) return "playmode_transition";
    return "idle";
}

dsarno and others added 2 commits January 20, 2026 17:58
Addresses four issues raised in code review:

1. EditorStateCache.GetSnapshot() - Remove shared cached clone
   - Revert to always returning fresh DeepClone() to prevent mutation bugs
   - Main GC optimization remains: state-change detection prevents
     unnecessary _cached rebuilds (the expensive operation)
   - Removed _cachedClone and _cachedCloneSequence fields

2. refresh_unity.py - Fix blocking reason terminology mismatch
   - Changed "asset_refresh" to "asset_import" to match activityPhase
     values from EditorStateCache.cs
   - Ensures asset import is correctly detected as blocking state

3. TransportCommandDispatcher - Fix unsynchronized Count access
   - Moved Pending.Count check inside PendingLock
   - Prevents data races and InvalidOperationException from concurrent
     dictionary access

4. StdioBridgeHost - Fix unsynchronized Count access
   - Moved commandQueue.Count check inside lockObj
   - Ensures all collection access is properly serialized

All changes maintain the GC allocation optimizations while fixing
thread safety violations and semantic contract changes.

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
- refresh_unity.py: Track readiness explicitly and return failure on timeout
  instead of silently returning success when wait loop exits without confirming
  ready_for_tools=true

- McpClientConfiguratorBase.cs: Add thread safety guard for Configure() call
  in CheckStatusWithProjectDir(). Changed default attemptAutoRewrite to false
  and added runtime check to prevent calling Configure() from background threads.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@dsarno dsarno merged commit 8eb684d into CoplayDev:main Jan 21, 2026
1 of 2 checks passed
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Server/src/services/tools/refresh_unity.py (1)

146-153: Don’t claim readiness when wait_for_ready=False.
If the caller opts out of waiting, the success message should avoid stating that the editor is ready.

✅ Proposed fix
-    if recovered_from_disconnect:
-        return MCPResponse(
-            success=True,
-            message="Refresh recovered after Unity disconnect/retry; editor is ready.",
-            data={"recovered_from_disconnect": True},
-        )
+    if recovered_from_disconnect:
+        ready_flag = bool(wait_for_ready and ready_confirmed)
+        msg = (
+            "Refresh recovered after Unity disconnect/retry; editor is ready."
+            if ready_flag
+            else "Refresh triggered; Unity may still be reloading."
+        )
+        return MCPResponse(
+            success=True,
+            message=msg,
+            data={"recovered_from_disconnect": True, "ready_confirmed": ready_flag},
+        )
🤖 Fix all issues with AI agents
In `@MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs`:
- Around line 436-439: Replace the unreliable main-thread check in
McpClientConfiguratorBase (where it currently uses
System.Threading.Thread.CurrentThread.ManagedThreadId == 1) with a cached main
thread id: add a static readonly int MainThreadId field initialized on class
load from System.Threading.Thread.CurrentThread.ManagedThreadId, then change the
check to compare the current thread's ManagedThreadId to MainThreadId before
attempting attemptAutoRewrite/Configure(); alternatively mention using
UnityEditor thread APIs if available for Editor-only validation.
🧹 Nitpick comments (1)
MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs (1)

582-648: Consider consolidating Register/Unregister to reduce duplication.

Register() and RegisterWithCapturedValues() share substantial logic. Similarly for the Unregister pair. Consider having the main-thread methods capture their values and delegate to the captured-values variants.

♻️ Example consolidation for Register
 private void Register()
 {
     var pathService = MCPServiceLocator.Paths;
     string claudePath = pathService.GetClaudeCliPath();
     if (string.IsNullOrEmpty(claudePath))
     {
         throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first.");
     }

     bool useHttpTransport = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true);
-
-    string args;
-    if (useHttpTransport)
-    {
-        string httpUrl = HttpEndpointUtility.GetMcpRpcUrl();
-        args = $"mcp add --transport http UnityMCP {httpUrl}";
-    }
-    else
-    {
-        var (uvxPath, gitUrl, packageName) = AssetPathUtility.GetUvxCommandParts();
-        // ... rest of stdio setup
-    }
-
     string projectDir = Path.GetDirectoryName(Application.dataPath);
-
     string pathPrepend = null;
     // ... pathPrepend setup ...
-
-    // Remove any existing registrations ...
-    // Now add the registration ...
-    // ... rest of implementation
+
+    string httpUrl = useHttpTransport ? HttpEndpointUtility.GetMcpRpcUrl() : null;
+    var (uvxPath, gitUrl, packageName) = useHttpTransport 
+        ? (null, null, null) 
+        : AssetPathUtility.GetUvxCommandParts();
+    bool shouldForceRefresh = !useHttpTransport && AssetPathUtility.ShouldForceUvxRefresh();
+
+    RegisterWithCapturedValues(projectDir, claudePath, pathPrepend,
+        useHttpTransport, httpUrl, uvxPath, gitUrl, packageName, shouldForceRefresh);
 }

Comment on lines +436 to +439
// Configure() requires main thread (accesses EditorPrefs, Application.dataPath)
// Only attempt auto-rewrite if we're on the main thread
bool isMainThread = System.Threading.Thread.CurrentThread.ManagedThreadId == 1;
if (attemptAutoRewrite && isMainThread)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unreliable main thread detection.

ManagedThreadId == 1 is not a reliable way to detect Unity's main thread. The main thread's ID is not guaranteed to be 1 across all platforms and Unity versions.

Consider using Unity's built-in mechanism or caching the main thread ID at initialization.

🔧 Suggested fix using a cached main thread ID pattern

Add a static field to cache the main thread ID (set during static initialization on the main thread):

// Add near the top of the class
private static readonly int MainThreadId = System.Threading.Thread.CurrentThread.ManagedThreadId;

Then replace the check:

-bool isMainThread = System.Threading.Thread.CurrentThread.ManagedThreadId == 1;
+bool isMainThread = System.Threading.Thread.CurrentThread.ManagedThreadId == MainThreadId;

Alternatively, if Unity's UnityEditor namespace is available, consider using editor-specific APIs to verify thread context.

🤖 Prompt for AI Agents
In `@MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs` around lines 436 -
439, Replace the unreliable main-thread check in McpClientConfiguratorBase
(where it currently uses System.Threading.Thread.CurrentThread.ManagedThreadId
== 1) with a cached main thread id: add a static readonly int MainThreadId field
initialized on class load from
System.Threading.Thread.CurrentThread.ManagedThreadId, then change the check to
compare the current thread's ManagedThreadId to MainThreadId before attempting
attemptAutoRewrite/Configure(); alternatively mention using UnityEditor thread
APIs if available for Editor-only validation.

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