Skip to content

Conversation

@dsarno
Copy link
Owner

@dsarno dsarno commented Jan 20, 2026

Summary by CodeRabbit

  • New Features
    • Async CLI-based configuration flow for Claude Code with background registration and safer auto-reregistration on transport/version mismatches; configurable retry behavior during reloads.
  • Bug Fixes
    • Clear stale resume state when stopping connections.
    • Windows-safe log rollover to tolerate locked files.
    • Treat some compile-time disconnects as recoverable.
  • Performance
    • Reduced per-frame allocations, cached editor state, and throttled editor updates.
  • Documentation
    • Clarified that uvx does not support --reinstall; recommend --no-cache --refresh.

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

dsarno and others added 3 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Jan 20, 2026

Walkthrough

Switches Claude Code configurator to a CLI-based configurator with thread-safe, main-thread-captured async configuration; updates migration and retry behavior for stdio/HTTP and reload cases; adds editor throttling and early-exit queue guards; improves refresh recovery and Windows-safe log rollover; small docs/comments updates.

Changes

Cohort / File(s) Summary
Claude CLI configurator & internal APIs
MCPForUnity/Editor/Clients/Configurators/ClaudeCodeConfigurator.cs, MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
ClaudeCodeConfigurator now inherits ClaudeCliMcpConfigurator. Added captured-value, thread-safe APIs: ConfigureWithCapturedValues, RegisterWithCapturedValues, UnregisterWithCapturedValues, ExtractPackageSourceFromCliOutput. Changed CheckStatusWithProjectDir default for attemptAutoRewrite to false and added transport-aware status checks.
Client config UI (async flow)
MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
UI delegates Claude CLI configuration to background Tasks after capturing main-thread values via new ConfigureClaudeCliAsync, shows contextual “checking” messages, prevents concurrent CLI ops per client, and updates status/UI on completion or error.
Migration flow
MCPForUnity/Editor/Migrations/StdIoVersionMigration.cs
Reworked gating to specially handle ClaudeCliMcpConfigurator (auto-check + possible rewrite) before applying JSON/file-based stdio eligibility checks.
Editor state & throttling
MCPForUnity/Editor/Services/EditorStateCache.cs, MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
Increased update throttle; added cheap pre-build change detection to avoid snapshot builds; OnEditorUpdate throttled to 2s to reduce per-frame expensive checks.
Transport / queue optimizations
MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs, MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs
Added early-return guards inside locks to avoid per-frame allocations when pending/queue is empty and duplicated-in-lock checks to reduce race allocations.
Connection resume cleanup
MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs
Clear resume-related EditorPrefs keys before stopping sessions and when ending orphaned sessions to avoid stale “Resuming…” UI state.
uvx docs & small comments
MCPForUnity/Editor/Helpers/AssetPathUtility.cs, MCPForUnity/Editor/Helpers/CodexConfigHelper.cs, MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs, MCPForUnity/Editor/Services/ServerManagementService.cs
Documentation/comments clarifying uvx does not support --reinstall and recommending --no-cache --refresh; no behavior changes.
Installation instructions
MCPForUnity/Editor/Clients/Configurators/ClaudeCodeConfigurator.cs
Updated GetInstallationSteps to reflect CLI-based registration and Claude Code CLI usage (replaces prior UI-guided steps).
Server: logging & packaging
Server/pyproject.toml, Server/src/main.py
Add setuptools package discovery config; introduce WindowsSafeRotatingFileHandler to swallow PermissionError during rollover on Windows.
refresh_unity recovery & retry control
Server/src/services/tools/refresh_unity.py, Server/src/transport/legacy/unity_connection.py
Normalize MCP responses, treat compile-time disconnects as potentially recoverable with a wait-for-ready loop; add max_attempts to UnityConnection.send_command and propagate new retry_on_reload flag through retry helpers to control retries during reloads.

Sequence Diagram(s)

sequenceDiagram
    participant UI as Editor UI
    participant Main as Main Thread
    participant Bg as Background Task
    participant CLI as Claude CLI Configurator
    participant Cache as Status / EditorPrefs

    UI->>Main: OnConfigureClicked()
    Main->>Main: Capture projectDir, claudePath, httpUrl, uvxPath, gitUrl, packageName, flags
    Main->>UI: Show "Checking..." (custom message)
    Main->>Bg: Start ConfigureClaudeCliAsync()
    Bg->>CLI: ConfigureWithCapturedValues(...) / UnregisterWithCapturedValues(...)
    CLI->>CLI: Build args, remove legacy regs, run Claude CLI, parse output
    CLI->>Cache: Read/write registration info
    CLI-->>Bg: Success / Error
    Bg->>Main: Invoke UI update (refresh status without recheck)
    Main->>UI: Update status UI / Show error if any
Loading
sequenceDiagram
    participant Caller as refresh_unity caller
    participant Retry as send_command_with_retry
    participant Conn as UnityConnection
    participant Unity as Unity Editor
    participant State as EditorState (ready checks)

    Caller->>Retry: send_command_with_retry(command, retry_on_reload=true)
    Retry->>Conn: send_command(..., max_attempts)
    Conn->>Unity: Send command
    alt Normal response
        Unity-->>Conn: Response
        Conn-->>Retry: Success
        Retry-->>Caller: Return success
    else Disconnect / domain reload
        Unity->>Unity: Domain reload / disconnected
        Conn-->>Retry: Error (disconnected)
        Retry->>Caller: Mark recoverable / hint retry
        Caller->>State: Poll until ready or timeout
        alt Ready before timeout
            Caller->>Retry: Retry send_command
            Retry->>Conn: Resend
            Conn-->>Caller: Success
        else Timeout
            Retry-->>Caller: Return timeout failure
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Http improvements #108: Overlaps changes to ClaudeCliMcpConfigurator status/registration and captured-value/thread-safe configuration helpers.

Poem

🐰 I hopped in code with nimble paws,

Captured values safe from main-thread laws,
Tasks hum gently in the background night,
Queues skip work and logs sleep tight,
Unity wakes — the rabbit gives a cheer!

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
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.
Title check ❓ Inconclusive The title 'Fix/577 performance optimizations' is vague and generic, using the term 'performance optimizations' without conveying what specific optimizations were made or which components were affected. Consider a more specific title that highlights the main optimization(s), such as 'Add early returns to prevent empty-list allocations' or 'Optimize editor state cache with change detection' to clarify the primary change.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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.

@greptile-apps
Copy link

greptile-apps bot commented Jan 20, 2026

Greptile Summary

This PR implements comprehensive performance optimizations addressing issue CoplayDev#577 with three main improvements:

Performance Optimizations:

  • Eliminated per-frame GC allocations in EditorStateCache.cs by implementing early state-change detection (checking primitive values before expensive JSON snapshot builds), caching DeepClone() results to avoid repeated allocations, and reducing update frequency from 0.25s to 1s
  • Fixed multiple domain reload loops by adding retry_on_reload=False parameter to refresh_unity, preventing the command from retrying when Unity disconnects during compilation (connection loss during domain reload is now correctly treated as success)
  • Improved UI responsiveness by making Claude CLI configuration async with proper main-thread value capture, preventing editor freezes during registration/unregistration

Additional Improvements:

  • Added Windows-safe log rotation handler to prevent crashes from file locking
  • Added support for legacy unityMCP naming alongside UnityMCP
  • Implemented auto-detection and reregistration for transport/package version mismatches
  • Enhanced wait loop logic to distinguish between actual blocking states vs stale status

The changes are well-structured with clear comments explaining the performance rationale. The optimizations target the root causes: unnecessary allocations, retry loops, and blocking UI operations.

Confidence Score: 4/5

  • This PR is safe to merge with minor risk from the connection loss handling logic change
  • The performance optimizations are well-designed and address real issues (GC spikes, retry loops, UI freezing). The caching and early-exit logic in EditorStateCache is sound. The async configuration properly captures main-thread values. However, the confidence is 4/5 rather than 5/5 because the refresh_unity.py change introduces new logic that treats connection loss as success when compile == "request", which could potentially mask real connection failures in edge cases. The logic appears correct for the intended use case (domain reload causing disconnect), but warrants careful testing to ensure legitimate connection errors aren't being silently treated as success.
  • Pay close attention to Server/src/services/tools/refresh_unity.py - verify the connection loss handling logic works correctly in both success and actual failure scenarios

Important Files Changed

Filename Overview
MCPForUnity/Editor/Services/EditorStateCache.cs Optimized state tracking with early change detection and cached clones to eliminate GC spikes (reduced update frequency from 0.25s to 1s, added state tracking to avoid expensive BuildSnapshot calls, cached DeepClone results)
Server/src/services/tools/refresh_unity.py Fixed multiple domain reload issue by disabling retry_on_reload for refresh_unity command and treating connection loss during compile as success (prevents retry loops)
Server/src/transport/legacy/unity_connection.py Added retry_on_reload parameter to control retry behavior during domain reloads, enabling commands that trigger reloads to disable retries
MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs Made Claude CLI configuration async with proper main-thread value capture to prevent UI freezing during registration/unregistration
MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs Added thread-safe Configure methods, legacy naming support (unityMCP), package version mismatch detection, and auto-reregistration for transport/version mismatches
Server/src/main.py Added WindowsSafeRotatingFileHandler to gracefully handle Windows file locking during log rotation

Sequence Diagram

sequenceDiagram
    participant Client as Claude Code
    participant Server as Python MCP Server
    participant Unity as Unity Editor
    
    Note over Unity: Performance Issue: GC spikes every frame
    
    Client->>Server: refresh_unity(compile="request")
    Server->>Unity: send_command("refresh_unity")
    Note over Server: retry_on_reload=False (NEW)
    Unity->>Unity: Trigger compilation
    Unity--xServer: Connection closed (domain reload)
    Note over Server: Treats connection loss as SUCCESS<br/>(prevents retry loop)
    Server->>Client: Success (recovered_from_disconnect)
    
    Note over Unity: Domain reload completes
    
    loop Wait for ready (if enabled)
        Server->>Unity: get_editor_state()
        Unity->>Server: editor state
        alt ready_for_tools == true
            Note over Server: Exit wait loop
        else blocking_reasons not in real_blocking_reasons
            Note over Server: Exit wait loop (not actually busy)
        else still busy
            Note over Server: Sleep 0.25s and retry
        end
    end
    
    Note over Unity: Performance Fix: Reduced GC allocations
    
    loop Editor Update Loop (every frame)
        Unity->>Unity: OnUpdate() called
        alt Less than 1s since last update
            Note over Unity: Skip update (was 0.25s)
        else Time for update
            Unity->>Unity: Check state variables<br/>(scene, focus, play mode, etc.)
            alt No state changes detected
                Note over Unity: Skip expensive BuildSnapshot()<br/>(KEY OPTIMIZATION)
            else State changed
                Unity->>Unity: BuildSnapshot()
                Unity->>Unity: Update tracked state
            end
        end
    end
    
    Client->>Server: Poll editor state
    Server->>Unity: GetSnapshot()
    Note over Unity: Return cached clone<br/>(avoids DeepClone allocations)
    Unity->>Server: Cached JObject
    Server->>Client: Editor state
    
    Note over Client,Unity: UI Stability Improvements
    
    Client->>Unity: Configure Claude CLI (async)
    Note over Unity: Capture main-thread values
    Unity->>Unity: Task.Run (background thread)
    Unity->>Unity: ConfigureWithCapturedValues()
    Note over Unity: No main thread blocking
    Unity->>Unity: EditorApplication.delayCall
    Unity->>Client: Configuration complete
Loading

Copy link

@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: 3

Caution

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

⚠️ Outside diff range comments (2)
MCPForUnity/Editor/Services/Transport/TransportCommandDispatcher.cs (1)

221-246: Avoid lock-free reads of Pending.Count.

Pending is a Dictionary<string, PendingCommand> mutated under PendingLock. Reading Dictionary.Count without holding the lock while background threads may modify the dictionary is not thread-safe—this can cause incorrect values, runtime corruption, or exceptions. The code's comment claiming the race is "harmless" does not align with C# thread-safety guarantees for Dictionary.

Consider removing the lock-free check (the inner check at line 243 already prevents allocations) or switch to ConcurrentDictionary<string, PendingCommand> which provides thread-safe Count and IsEmpty properties.

🛠️ Minimal safe fix (remove lock-free check)
-            // Early exit BEFORE acquiring lock or allocating. This prevents the per-frame
-            // List allocations that trigger GC every ~1 second (GitHub issue `#577`).
-            // 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;
-            }

             if (Interlocked.Exchange(ref _processingFlag, 1) == 1)
MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs (1)

394-466: Default attemptAutoRewrite = true creates design risk despite current safeguards

While CheckStatusWithProjectDir is properly documented as thread-safe and the current only background call site explicitly passes attemptAutoRewrite: false to avoid calling main-thread-only Configure(), the default parameter value is a risky API design. Future developers might bypass this safeguard without noticing the threading constraint. Consider changing the default to attemptAutoRewrite = false or renaming the background-safe variant to make the threading contract more explicit at the call site.

🤖 Fix all issues with AI agents
In `@MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs`:
- Around line 821-835: Remove the unsafe lock-free read of commandQueue.Count:
delete the outer if (commandQueue.Count == 0) early-return and rely on the inner
double-check inside the lock on lockObj that already prevents allocations;
alternatively, if you intend a lock-free check use a thread-safe structure such
as ConcurrentDictionary and call its IsEmpty, but do not read commandQueue.Count
outside the lock. Locate the code in StdioBridgeHost where commandQueue
(Dictionary<string, QueuedCommand>) and lockObj are used to populate
List<(string id, QueuedCommand)>, and remove or replace the outer check to
ensure all reads of commandQueue.Count occur under lockObj or via a thread-safe
API.

In `@Server/src/main.py`:
- Around line 42-52: The current doRollover in WindowsSafeRotatingFileHandler
silently swallows PermissionError after calling super().doRollover(), which can
leave self.stream closed and the handler unusable; change the handler to catch
PermissionError around the file-operation portion and, when PermissionError
occurs, ensure the handler's stream is restored before returning (e.g., if
self.stream is missing or closed, reopen the log file into self.stream using
self.baseFilename and the handler's mode/encoding/settings so future emit()
calls continue to work), and re-raise other unexpected exceptions instead of
suppressing them; reference WindowsSafeRotatingFileHandler.doRollover,
RotatingFileHandler, self.stream and self.baseFilename when making the fix.

In `@Server/src/transport/legacy/unity_connection.py`:
- Around line 236-249: Update the type annotations in send_command: change the
params type from dict[str, Any] to dict[str, Any] | None and update the return
annotation to reflect it can return either a raw dict or an MCPResponse (e.g.,
dict[str, Any] | MCPResponse or a suitable union/alias used in the codebase).
Adjust any imports/typing aliases if needed and ensure references to MCPResponse
in the function (return statements around the early params None guard and later
return points) are compatible with the new return annotation.
🧹 Nitpick comments (2)
MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs (1)

676-682: Consider extracting resume-flag clearing into a helper to avoid duplication.

Both blocks do the same work; a small helper improves maintainability and keeps any future logging consistent.

♻️ Proposed refactor
+        private static void ClearResumeFlags()
+        {
+            try { EditorPrefs.DeleteKey(EditorPrefKeys.ResumeStdioAfterReload); } catch { }
+            try { EditorPrefs.DeleteKey(EditorPrefKeys.ResumeHttpAfterReload); } catch { }
+        }
...
-                    try { EditorPrefs.DeleteKey(EditorPrefKeys.ResumeStdioAfterReload); } catch { }
-                    try { EditorPrefs.DeleteKey(EditorPrefKeys.ResumeHttpAfterReload); } catch { }
+                    ClearResumeFlags();
...
-                try { EditorPrefs.DeleteKey(EditorPrefKeys.ResumeStdioAfterReload); } catch { }
-                try { EditorPrefs.DeleteKey(EditorPrefKeys.ResumeHttpAfterReload); } catch { }
+                ClearResumeFlags();

Also applies to: 724-730

Server/src/services/tools/refresh_unity.py (1)

60-61: Consider explicit success check instead of defaulting to True.

Using response_dict.get("success", True) means a response without a success key is treated as successful. While this may be intentional for backward compatibility, it could silently skip error handling for malformed responses.

Additionally, the response.__dict__ fallback on line 60 may include internal Python attributes if response is neither a dict nor has model_dump(). Consider using a more explicit conversion or raising an error for unexpected response types.

💡 Suggested improvement
-    response_dict = response if isinstance(response, dict) else (response.model_dump() if hasattr(response, "model_dump") else response.__dict__)
-    if not response_dict.get("success", True):
+    if isinstance(response, dict):
+        response_dict = response
+    elif hasattr(response, "model_dump"):
+        response_dict = response.model_dump()
+    else:
+        # Unexpected response type - treat as error
+        return MCPResponse(success=False, error=f"Unexpected response type: {type(response).__name__}")
+    
+    if not response_dict.get("success", False):

Comment on lines +42 to +52
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
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Silent suppression may leave handler in broken state.

The base doRollover() closes the stream before attempting file renames. If PermissionError occurs mid-rollover, the stream may already be closed, leaving subsequent writes to fail silently or raise exceptions.

Consider a more defensive approach that ensures the stream remains usable:

Proposed fix
 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
+            # Skip rotation this time - we'll try again on the next rollover.
+            # Ensure stream is still open for continued logging.
+            if self.stream is None or self.stream.closed:
+                self.stream = self._open()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
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.
# Ensure stream is still open for continued logging.
if self.stream is None or self.stream.closed:
self.stream = self._open()
🤖 Prompt for AI Agents
In `@Server/src/main.py` around lines 42 - 52, The current doRollover in
WindowsSafeRotatingFileHandler silently swallows PermissionError after calling
super().doRollover(), which can leave self.stream closed and the handler
unusable; change the handler to catch PermissionError around the file-operation
portion and, when PermissionError occurs, ensure the handler's stream is
restored before returning (e.g., if self.stream is missing or closed, reopen the
log file into self.stream using self.baseFilename and the handler's
mode/encoding/settings so future emit() calls continue to work), and re-raise
other unexpected exceptions instead of suppressing them; reference
WindowsSafeRotatingFileHandler.doRollover, RotatingFileHandler, self.stream and
self.baseFilename when making the fix.

Comment on lines +236 to +249
def send_command(self, command_type: str, params: dict[str, Any] = None, max_attempts: int | None = None) -> dict[str, Any]:
"""Send a command with retry/backoff and port rediscovery. Pings only when requested.
Args:
command_type: The Unity command to send
params: Command parameters
max_attempts: Maximum retry attempts (None = use config default, 0 = no retries)
"""
# Defensive guard: catch empty/placeholder invocations early
if not command_type:
raise ValueError("MCP call missing command_type")
if params is None:
return MCPResponse(success=False, error="MCP call received with no parameters (client placeholder?)")
attempts = max(config.max_retries, 5)
attempts = max(config.max_retries, 5) if max_attempts is None else max_attempts
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat -n Server/src/transport/legacy/unity_connection.py | sed -n '230,260p'

Repository: dsarno/unity-mcp

Length of output: 1787


🏁 Script executed:

rg "class MCPResponse" --type py -A 5

Repository: dsarno/unity-mcp

Length of output: 429


🏁 Script executed:

cat -n Server/src/transport/legacy/unity_connection.py | sed -n '236,350p'

Repository: dsarno/unity-mcp

Length of output: 6264


🏁 Script executed:

cat -n Server/src/transport/legacy/unity_connection.py | sed -n '350,420p'

Repository: dsarno/unity-mcp

Length of output: 4065


🏁 Script executed:

cat -n Server/src/transport/legacy/unity_connection.py | sed -n '420,460p'

Repository: dsarno/unity-mcp

Length of output: 1841


Make params explicitly optional in the type hint.

params defaults to None, so the annotation should be dict[str, Any] | None to satisfy PEP 484 and silence RUF013. Additionally, the function's return type annotation (dict[str, Any]) is inconsistent with actual return statements on lines 248 and 297–301, which return MCPResponse objects. The return type should be updated to reflect both dict[str, Any] and MCPResponse (or a union thereof).

🔧 Proposed fix for params
-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]:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def send_command(self, command_type: str, params: dict[str, Any] = None, max_attempts: int | None = None) -> dict[str, Any]:
"""Send a command with retry/backoff and port rediscovery. Pings only when requested.
Args:
command_type: The Unity command to send
params: Command parameters
max_attempts: Maximum retry attempts (None = use config default, 0 = no retries)
"""
# Defensive guard: catch empty/placeholder invocations early
if not command_type:
raise ValueError("MCP call missing command_type")
if params is None:
return MCPResponse(success=False, error="MCP call received with no parameters (client placeholder?)")
attempts = max(config.max_retries, 5)
attempts = max(config.max_retries, 5) if max_attempts is None else max_attempts
def send_command(self, command_type: str, params: dict[str, Any] | None = None, max_attempts: int | None = None) -> dict[str, Any]:
"""Send a command with retry/backoff and port rediscovery. Pings only when requested.
Args:
command_type: The Unity command to send
params: Command parameters
max_attempts: Maximum retry attempts (None = use config default, 0 = no retries)
"""
# Defensive guard: catch empty/placeholder invocations early
if not command_type:
raise ValueError("MCP call missing command_type")
if params is None:
return MCPResponse(success=False, error="MCP call received with no parameters (client placeholder?)")
attempts = max(config.max_retries, 5) if max_attempts is None else max_attempts
🧰 Tools
🪛 Ruff (0.14.13)

236-236: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)


246-246: Avoid specifying long messages outside the exception class

(TRY003)

🤖 Prompt for AI Agents
In `@Server/src/transport/legacy/unity_connection.py` around lines 236 - 249,
Update the type annotations in send_command: change the params type from
dict[str, Any] to dict[str, Any] | None and update the return annotation to
reflect it can return either a raw dict or an MCPResponse (e.g., dict[str, Any]
| MCPResponse or a suitable union/alias used in the codebase). Adjust any
imports/typing aliases if needed and ensure references to MCPResponse in the
function (return statements around the early params None guard and later return
points) are compatible with the new return annotation.

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 <noreply@anthropic.com>
Copy link

@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 (1)
Server/src/services/tools/refresh_unity.py (1)

1-16: Missing logger import causes runtime NameError.

The static analysis correctly identified that logger is used on lines 82 and 91 but is never imported or defined. This will cause a NameError at runtime when the connection-loss handling paths are triggered.

🐛 Proposed fix: Add logger import
 from __future__ import annotations
 
 import asyncio
+import logging
 import time
 from typing import Annotated, Any, Literal
 
 from fastmcp import Context
 from mcp.types import ToolAnnotations
 
 from models import MCPResponse
 from services.registry import mcp_for_unity_tool
 from services.tools import get_unity_instance_from_context
 import transport.unity_transport as unity_transport
 from transport.legacy.unity_connection import async_send_command_with_retry, _extract_response_reason
 from services.state.external_changes_scanner import external_changes_scanner
 import services.resources.editor_state as editor_state
+
+logger = logging.getLogger(__name__)

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 <noreply@anthropic.com>
Copy link

@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)

103-122: Avoid exiting the wait loop on unknown blocking reasons.

if not (blocking & real_blocking_reasons) breaks even when Unity reports new busy states not in the set (e.g., future blocking reasons). That can return before readiness is restored. Consider only bypassing when blocking reasons are empty or just stale_status.

🔧 Suggested guard
-                blocking = set(advice.get("blocking_reasons") or [])
-                if not (blocking & real_blocking_reasons):
-                    break
+                blocking = set(advice.get("blocking_reasons") or [])
+                non_blocking = {"stale_status"}
+                if not blocking or blocking <= non_blocking:
+                    break
🤖 Fix all issues with AI agents
In `@Server/src/services/tools/refresh_unity.py`:
- Line 140: The function's annotated return type still says "MCPResponse |
dict[str, Any]" but the implementation always converts dict responses into an
MCPResponse (return MCPResponse(**response_dict) if isinstance(response, dict)
else response), so update the function signature to "-> MCPResponse" to match
behavior (replace the union return annotation). Ensure the annotation change
references MCPResponse and leave the body intact; follow the same pattern used
in execute_menu_item.py for consistency.

)

return MCPResponse(**response) if isinstance(response, dict) else response
return MCPResponse(**response_dict) if isinstance(response, dict) else response
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's understand the context around line 140
cd Server/src/services/tools && wc -l refresh_unity.py

Repository: dsarno/unity-mcp

Length of output: 80


🏁 Script executed:

# Read the file around line 140 to find the function and understand context
cd Server/src/services/tools && sed -n '115,155p' refresh_unity.py

Repository: dsarno/unity-mcp

Length of output: 1217


🏁 Script executed:

# Find the complete function definition - look for def statements before line 140
cd Server/src/services/tools && sed -n '1,140p' refresh_unity.py | tail -50

Repository: dsarno/unity-mcp

Length of output: 2417


🏁 Script executed:

# Search for all call sites of refresh_unity function in the repository
rg -nP --type=py 'refresh_unity\s*\(' --max-count=20

Repository: dsarno/unity-mcp

Length of output: 578


🏁 Script executed:

# Also check for any type imports and the MCPResponse class definition
rg -nP --type=py 'MCPResponse|from.*import.*MCPResponse' --max-count=20

Repository: dsarno/unity-mcp

Length of output: 11011


🏁 Script executed:

# Check what happens with the response in test_refresh_unity_retry_recovery.py
sed -n '30,40p' Server/tests/integration/test_refresh_unity_retry_recovery.py

Repository: dsarno/unity-mcp

Length of output: 567


🏁 Script executed:

# Check what happens with the response in preflight.py
sed -n '65,80p' Server/src/services/tools/preflight.py

Repository: dsarno/unity-mcp

Length of output: 809


🏁 Script executed:

# Get the full function signature and return statement context in refresh_unity.py
sed -n '29,45p' Server/src/services/tools/refresh_unity.py

Repository: dsarno/unity-mcp

Length of output: 793


🏁 Script executed:

# Also check all return statements in the function to understand what types can be returned
rg -nP 'return' Server/src/services/tools/refresh_unity.py | head -20

Repository: dsarno/unity-mcp

Length of output: 485


Update return type annotation to match implementation.

The function now always returns MCPResponse (line 140 wraps dict responses), but the annotation at line 38 still declares MCPResponse | dict[str, Any]. This creates a contract violation—type checkers and callers relying on the annotation will expect dict responses that will never arrive.

Either update the annotation to -> MCPResponse: (matching the actual behavior) or revert line 140 to preserve the prior dict return for backward compatibility. For consistency with the rest of the function and similar patterns elsewhere (e.g., execute_menu_item.py), updating the annotation is the correct fix.

🤖 Prompt for AI Agents
In `@Server/src/services/tools/refresh_unity.py` at line 140, The function's
annotated return type still says "MCPResponse | dict[str, Any]" but the
implementation always converts dict responses into an MCPResponse (return
MCPResponse(**response_dict) if isinstance(response, dict) else response), so
update the function signature to "-> MCPResponse" to match behavior (replace the
union return annotation). Ensure the annotation change references MCPResponse
and leave the body intact; follow the same pattern used in execute_menu_item.py
for consistency.

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 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
Copy link

@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: 3

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-151: Avoid asserting readiness when wait_for_ready=False.

recovered_from_disconnect can be true even when readiness wasn’t checked, but the response message claims “editor is ready.” Consider tailoring the message (or payload) based on wait_for_ready/ready_confirmed.

💡 Suggested tweak
-    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:
+        message = (
+            "Refresh recovered after Unity disconnect/retry; editor is ready."
+            if wait_for_ready
+            else "Refresh recovered after Unity disconnect/retry; editor readiness not confirmed."
+        )
+        return MCPResponse(
+            success=True,
+            message=message,
+            data={"recovered_from_disconnect": True, "ready_confirmed": bool(wait_for_ready)},
+        )
🤖 Fix all issues with AI agents
In `@MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs`:
- Around line 526-576: The RegisterWithCapturedValues method currently only
validates claudePath; add fast-fail guard clauses at the start of
RegisterWithCapturedValues to validate projectDir is not null/empty and,
depending on useHttpTransport, that httpUrl (when useHttpTransport==true) or
uvxPath, gitUrl and packageName (when useHttpTransport==false) are not
null/empty, throwing InvalidOperationException with clear messages; similarly
add a simple guard in UnregisterWithCapturedValues to validate projectDir (and
claudePath already checked) before calling ExecPath.TryRun so failures surface
with explicit diagnostics instead of cryptic CLI errors—place these checks
before building args and before any ExecPath.TryRun calls in the respective
methods.
- Around line 436-440: The current main-thread check in
CheckStatusWithProjectDir uses a hardcoded ManagedThreadId == 1 which is
unreliable; change it to compare against a captured main-thread ID stored at
startup (e.g. a static readonly int MainThreadId or similar) instead of the
literal 1, and update the local isMainThread logic (where attemptAutoRewrite and
Configure() are used) to compare
System.Threading.Thread.CurrentThread.ManagedThreadId == MainThreadId; follow
the same capture pattern used in TransportCommandDispatcher.cs /
StdioBridgeHost.cs so auto-rewrite isn't falsely disabled across domain reloads.

In `@Server/src/services/tools/refresh_unity.py`:
- Around line 116-125: The current readiness logic treats any unknown
blocking_reasons as non-blocking because it checks if blocking &
real_blocking_reasons is empty; change this so unknown reasons are considered
blocking and only explicitly known non-blocking conditions short-circuit. Update
the block using the existing names (advice, blocking_reasons, blocking,
real_blocking_reasons, ready_confirmed) to compute blocking =
set(advice.get("blocking_reasons") or []) and then set ready_confirmed True only
when blocking is empty or when blocking is exactly the known non-blocking set
(e.g., {"stale_status"}) — do not treat unknown entries as non-blocking.
♻️ Duplicate comments (1)
Server/src/services/tools/refresh_unity.py (1)

153-153: Return type annotation still looks inconsistent with behavior.

The implementation now always returns an MCPResponse, but the signature still advertises MCPResponse | dict[str, Any] (see Line 38). Consider tightening the annotation to match the current behavior.

Comment on lines +436 to +440
// 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

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n "IsMainThread|CurrentThreadIsMainThread|SynchronizationContext|MainThread" -g '*.cs'

Repository: dsarno/unity-mcp

Length of output: 2079


🏁 Script executed:

# Also search for ManagedThreadId usage to see if this pattern is used elsewhere
rg -n "ManagedThreadId" -g '*.cs'

Repository: dsarno/unity-mcp

Length of output: 1037


🏁 Script executed:

# Look at the broader context of the file to understand how threading is handled
fd "McpClientConfiguratorBase.cs" -g '*.cs' --exec head -n 50 {}

Repository: dsarno/unity-mcp

Length of output: 151


🏁 Script executed:

cat -n MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs | head -100

Repository: dsarno/unity-mcp

Length of output: 4346


🏁 Script executed:

cat -n MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs | sed -n '420,460p'

Repository: dsarno/unity-mcp

Length of output: 3253


🏁 Script executed:

# Check the class declaration and any existing field initialization
rg -n "class McpClientConfiguratorBase|private|readonly.*Thread|_mainThread" MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs | head -30

Repository: dsarno/unity-mcp

Length of output: 464


🏁 Script executed:

rg -n "CheckStatus" MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs -B 2 -A 2

Repository: dsarno/unity-mcp

Length of output: 1913


🏁 Script executed:

# Search for callers of CheckStatus to see if it's called from background threads
rg "\.CheckStatus\(" --type cs -B 2 | head -50

Repository: dsarno/unity-mcp

Length of output: 1042


🏁 Script executed:

# Look at the broader context - the entire CheckStatusWithProjectDir method
cat -n MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs | sed -n '359,470p'

Repository: dsarno/unity-mcp

Length of output: 8052


Replace hardcoded ManagedThreadId comparison with a captured main-thread ID for reliability.

The check at line 438 uses ManagedThreadId == 1, which is not guaranteed across editor sessions or domain reloads. Since CheckStatusWithProjectDir explicitly supports background-thread execution (lines 352–353) but calls Configure() on the main thread (line 449), a false negative here silently disables auto-rewrite. The codebase already demonstrates the correct pattern elsewhere (TransportCommandDispatcher.cs, StdioBridgeHost.cs): capture the main thread ID at initialization and compare against it. Apply the same approach here to ensure reliable detection.

🤖 Prompt for AI Agents
In `@MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs` around lines 436 -
440, The current main-thread check in CheckStatusWithProjectDir uses a hardcoded
ManagedThreadId == 1 which is unreliable; change it to compare against a
captured main-thread ID stored at startup (e.g. a static readonly int
MainThreadId or similar) instead of the literal 1, and update the local
isMainThread logic (where attemptAutoRewrite and Configure() are used) to
compare System.Threading.Thread.CurrentThread.ManagedThreadId == MainThreadId;
follow the same capture pattern used in TransportCommandDispatcher.cs /
StdioBridgeHost.cs so auto-rewrite isn't falsely disabled across domain reloads.

Comment on lines +526 to +576
private void RegisterWithCapturedValues(
string projectDir, string claudePath, string pathPrepend,
bool useHttpTransport, string httpUrl,
string uvxPath, string gitUrl, string packageName, bool shouldForceRefresh)
{
if (string.IsNullOrEmpty(claudePath))
{
throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first.");
}

string args;
if (useHttpTransport)
{
args = $"mcp add --transport http UnityMCP {httpUrl}";
}
else
{
// Note: --reinstall is not supported by uvx, use --no-cache --refresh instead
string devFlags = shouldForceRefresh ? "--no-cache --refresh " : string.Empty;
args = $"mcp add --transport stdio UnityMCP -- \"{uvxPath}\" {devFlags}--from \"{gitUrl}\" {packageName}";
}

// Remove any existing registrations - handle both "UnityMCP" and "unityMCP" (legacy)
McpLog.Info("Removing any existing UnityMCP registrations before adding...");
ExecPath.TryRun(claudePath, "mcp remove UnityMCP", projectDir, out _, out _, 7000, pathPrepend);
ExecPath.TryRun(claudePath, "mcp remove unityMCP", projectDir, out _, out _, 7000, pathPrepend);

// Now add the registration
if (!ExecPath.TryRun(claudePath, args, projectDir, out var stdout, out var stderr, 15000, pathPrepend))
{
throw new InvalidOperationException($"Failed to register with Claude Code:\n{stderr}\n{stdout}");
}

McpLog.Info($"Successfully registered with Claude Code using {(useHttpTransport ? "HTTP" : "stdio")} transport.");
client.SetStatus(McpStatus.Configured);
}

/// <summary>
/// Thread-safe unregistration using pre-captured values.
/// </summary>
private void UnregisterWithCapturedValues(string projectDir, string claudePath, string pathPrepend)
{
if (string.IsNullOrEmpty(claudePath))
{
throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first.");
}

// Remove both "UnityMCP" and "unityMCP" (legacy naming)
McpLog.Info("Removing all UnityMCP registrations...");
ExecPath.TryRun(claudePath, "mcp remove UnityMCP", projectDir, out _, out _, 7000, pathPrepend);
ExecPath.TryRun(claudePath, "mcp remove unityMCP", projectDir, out _, out _, 7000, pathPrepend);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Validate captured values before building CLI args.

Only claudePath is validated; projectDir, httpUrl, and stdio parts can be null/empty, leading to misleading CLI failures or exceptions. Add fast-fail guards for clearer diagnostics.

✅ Suggested guard clauses
 private void RegisterWithCapturedValues(
     string projectDir, string claudePath, string pathPrepend,
     bool useHttpTransport, string httpUrl,
     string uvxPath, string gitUrl, string packageName, bool shouldForceRefresh)
 {
     if (string.IsNullOrEmpty(claudePath))
     {
         throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first.");
     }
+    if (string.IsNullOrEmpty(projectDir))
+    {
+        throw new ArgumentNullException(nameof(projectDir), "Project directory must be provided.");
+    }
+    if (useHttpTransport && string.IsNullOrEmpty(httpUrl))
+    {
+        throw new ArgumentException("HTTP URL must be provided for HTTP transport.", nameof(httpUrl));
+    }
+    if (!useHttpTransport &&
+        (string.IsNullOrEmpty(uvxPath) || string.IsNullOrEmpty(gitUrl) || string.IsNullOrEmpty(packageName)))
+    {
+        throw new ArgumentException("uvxPath, gitUrl, and packageName are required for stdio transport.");
+    }

     string args;
     if (useHttpTransport)
     {
         args = $"mcp add --transport http UnityMCP {httpUrl}";
     }
     else
     {
         // Note: --reinstall is not supported by uvx, use --no-cache --refresh instead
         string devFlags = shouldForceRefresh ? "--no-cache --refresh " : string.Empty;
         args = $"mcp add --transport stdio UnityMCP -- \"{uvxPath}\" {devFlags}--from \"{gitUrl}\" {packageName}";
     }
 }
 
 private void UnregisterWithCapturedValues(string projectDir, string claudePath, string pathPrepend)
 {
     if (string.IsNullOrEmpty(claudePath))
     {
         throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first.");
     }
+    if (string.IsNullOrEmpty(projectDir))
+    {
+        throw new ArgumentNullException(nameof(projectDir), "Project directory must be provided.");
+    }
🤖 Prompt for AI Agents
In `@MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs` around lines 526 -
576, The RegisterWithCapturedValues method currently only validates claudePath;
add fast-fail guard clauses at the start of RegisterWithCapturedValues to
validate projectDir is not null/empty and, depending on useHttpTransport, that
httpUrl (when useHttpTransport==true) or uvxPath, gitUrl and packageName (when
useHttpTransport==false) are not null/empty, throwing InvalidOperationException
with clear messages; similarly add a simple guard in
UnregisterWithCapturedValues to validate projectDir (and claudePath already
checked) before calling ExecPath.TryRun so failures surface with explicit
diagnostics instead of cryptic CLI errors—place these checks before building
args and before any ExecPath.TryRun calls in the respective methods.

Comment on lines +116 to +125
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 # No real blocking reasons, consider ready
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Treat unknown blocking reasons as blocking.

Right now any blocking_reasons not in real_blocking_reasons are treated as “ready,” which can allow tools to proceed while Unity is still busy (e.g., new/unknown reasons). Consider only short‑circuiting when the reasons are explicitly non-blocking (e.g., only stale_status).

🛠️ Safer readiness check
-                blocking = set(advice.get("blocking_reasons") or [])
-                if not (blocking & real_blocking_reasons):
-                    ready_confirmed = True  # No real blocking reasons, consider ready
-                    break
+                blocking = set(advice.get("blocking_reasons") or [])
+                allowed_nonblocking = {"stale_status"}
+                if blocking and not blocking.issubset(allowed_nonblocking):
+                    continue
+                if blocking.issubset(allowed_nonblocking):
+                    ready_confirmed = True
+                    break
🤖 Prompt for AI Agents
In `@Server/src/services/tools/refresh_unity.py` around lines 116 - 125, The
current readiness logic treats any unknown blocking_reasons as non-blocking
because it checks if blocking & real_blocking_reasons is empty; change this so
unknown reasons are considered blocking and only explicitly known non-blocking
conditions short-circuit. Update the block using the existing names (advice,
blocking_reasons, blocking, real_blocking_reasons, ready_confirmed) to compute
blocking = set(advice.get("blocking_reasons") or []) and then set
ready_confirmed True only when blocking is empty or when blocking is exactly the
known non-blocking set (e.g., {"stale_status"}) — do not treat unknown entries
as non-blocking.

@dsarno dsarno closed this Jan 21, 2026
@dsarno dsarno deleted the fix/577-performance-optimizations branch January 22, 2026 13:21
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