Skip to content

Conversation

@dsarno
Copy link
Collaborator

@dsarno dsarno commented Dec 30, 2025

Summary

This PR fixes critical issues with Claude Code registration in HTTP mode, including UI blocking,
thread safety violations, and transport mismatch detection.

Issues Fixed

1. UI Blocking During Registration

The registration process was blocking the Unity Editor UI due to synchronous status checks after
registration completed. Users would experience freezing when clicking the Register button.

Fix: Removed blocking CheckStatus() call after registration. Status is now set immediately to
Configured, with async verification running in the background.

2. Thread Safety Violations

The async status check was calling Unity main-thread-only APIs (Application.dataPath and
EditorPrefs.GetBool()) from background threads, causing errors:
GetBool can only be called from the main thread
get_dataPath can only be called from the main thread

Fix: Capture these values on the main thread before spawning async tasks, then pass them to
background threads via closure.

3. Transport Mismatch Not Detected

When switching between HTTP and stdio transport modes, the Claude Code registration status wasn't
immediately updated, showing green (configured) even when the registration was invalid.

Fix: Force immediate status check when transport mode changes, instead of waiting for the
45-second refresh interval.

4. Transport Validation Parsing

The Claude CLI output format wasn't being parsed correctly to detect HTTP vs stdio registration.

Fix: Updated to parse the correct CLI output format (Type: http / Type: stdio).

5. Stale Registration on Transport Change

When transport mode changed, the old registration wasn't being removed before re-registering.

Fix: Always remove existing UnityMCP registration before re-registering to ensure transport mode
is up-to-date.

Summary by Sourcery

Resolve Claude CLI-based MCP client registration and status issues, including transport detection, async status checks, and editor responsiveness.

Bug Fixes:

  • Prevent Unity Editor UI from blocking during Claude CLI registration by avoiding synchronous status checks after registration and unregister actions.
  • Fix thread-safety issues in async status checks by avoiding main-thread-only Unity API calls from background threads.
  • Ensure transport mode mismatches between HTTP and stdio registrations are detected and surfaced in client status based on Claude CLI output.
  • Ensure stale Claude Code registrations are removed before re-registering so the configured transport mode stays in sync.
  • Trigger an immediate status refresh when the transport protocol is changed so the UI reflects the current registration state.

Enhancements:

  • Extend Claude CLI client status checking to support injected project directory and transport mode values for use from background tasks.
  • Improve Claude CLI registration logging to indicate which transport protocol is used on successful registration.
  • Adjust client refresh logic to selectively force immediate status checks and centralize async refresh behavior for the Claude CLI client.

Summary by CodeRabbit

  • New Features

    • Added transport-change event to trigger immediate configuration refresh.
  • Improvements

    • Transport-aware status checks to validate configured transport type.
    • Streamlined register/unregister flow with cleanup-before-reregister and immediate configured state after successful registration.
    • Client refresh can now be forced immediately to reflect transport changes.
  • Bug Fixes

    • Fixed stale status refresh timing for CLI-based clients.

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

…stering

When registering with Claude Code, if a UnityMCP server already exists,
remove it first before adding the new registration. This ensures the
transport mode (HTTP vs stdio) is always updated to match the current
UseHttpTransport EditorPref setting.

Previously, if a stdio registration existed and the user tried to register
with HTTP, the command would fail with 'already exists' and the old stdio
configuration would remain unchanged.
…ctly

The validation code was incorrectly parsing the output of 'claude mcp get UnityMCP' by looking for JSON format ("transport": "http"), but the CLI actually returns human-readable text format ("Type: http"). This caused the transport mismatch detection to never trigger, allowing stdio to be selected in the UI while HTTP was registered with Claude Code.

Changes:
- Fix parsing logic to check for "Type: http" or "Type: stdio" in CLI output
- Add OnTransportChanged event to refresh client status when transport changes
- Wire up event handler to trigger client status refresh on transport dropdown change

This ensures that when the transport mode in Unity doesn't match what's registered with Claude Code, the UI will correctly show an error status with instructions to re-register.
…laude CLI status checks and HTTP simplifications
This commit resolves three issues with Claude Code registration:

1. UI blocking: Removed synchronous CheckStatus() call after registration
   that was blocking the editor. Status is now set immediately with async
   verification happening in the background.

2. Thread safety: Fixed "can only be called from the main thread" errors
   by capturing Application.dataPath and EditorPrefs.GetBool() on the main
   thread before spawning async status check tasks.

3. Transport mismatch detection: Transport mode changes now trigger immediate
   status checks to detect HTTP/stdio mismatches, instead of waiting for the
   45-second refresh interval.

The registration button now turns green immediately after successful
registration without blocking, and properly detects transport mismatches
when switching between HTTP and stdio modes.
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Dec 30, 2025

Reviewer's Guide

Refactors Claude CLI MCP status checking and registration to be non-blocking, thread-safe, and transport-aware, and wires the transport selector UI to trigger immediate, correct status refreshes when HTTP/stdio settings change.

Sequence diagram for transport change triggering immediate Claude CLI status refresh

sequenceDiagram
    actor User
    participant MCPForUnityEditorWindow
    participant McpConnectionSection
    participant McpClientConfigSection
    participant ClaudeCliMcpConfigurator
    participant BackgroundTask

    User->>McpConnectionSection: Change transport protocol
    McpConnectionSection-->>McpConnectionSection: UpdateHttpFieldVisibility()
    McpConnectionSection-->>McpConnectionSection: RefreshHttpUi()
    McpConnectionSection-->>MCPForUnityEditorWindow: OnManualConfigUpdateRequested
    MCPForUnityEditorWindow->>McpClientConfigSection: UpdateManualConfiguration()
    McpConnectionSection-->>MCPForUnityEditorWindow: OnTransportChanged
    MCPForUnityEditorWindow->>McpClientConfigSection: RefreshSelectedClient(forceImmediate=true)

    McpClientConfigSection-->>McpClientConfigSection: shouldForceImmediate = true
    McpClientConfigSection->>McpClientConfigSection: RefreshClientStatus(client, forceImmediate=true)
    McpClientConfigSection->>McpClientConfigSection: RefreshClaudeCliStatus(client, forceImmediate=true)

    alt First time or stale status
        McpClientConfigSection-->>McpClientConfigSection: needsRefresh = true
    else Recently checked and fresh
        McpClientConfigSection-->>McpClientConfigSection: needsRefresh may be false
    end

    McpClientConfigSection-->>McpClientConfigSection: Capture projectDir = Path.GetDirectoryName(Application.dataPath)
    McpClientConfigSection-->>McpClientConfigSection: Capture useHttpTransport = EditorPrefs.GetBool(UseHttpTransport)
    McpClientConfigSection-->>McpClientConfigSection: statusRefreshInFlight.Add(client)
    McpClientConfigSection-->>McpClientConfigSection: ApplyStatusToUi(showChecking=true)

    McpClientConfigSection->>BackgroundTask: Task.Run(CheckStatusWithProjectDir(projectDir, useHttpTransport))

    BackgroundTask->>ClaudeCliMcpConfigurator: CheckStatusWithProjectDir(projectDir, useHttpTransport, attemptAutoRewrite=false)
    ClaudeCliMcpConfigurator->>ClaudeCliMcpConfigurator: ExecPath.TryRun(claudePath, "mcp list")
    alt UnityMCP not found
        ClaudeCliMcpConfigurator-->>ClaudeCliMcpConfigurator: client.SetStatus(NotConfigured)
    else UnityMCP found
        ClaudeCliMcpConfigurator->>ClaudeCliMcpConfigurator: ExecPath.TryRun(claudePath, "mcp get UnityMCP")
        ClaudeCliMcpConfigurator-->>ClaudeCliMcpConfigurator: Parse Type http or Type stdio
        alt Transport mismatch
            ClaudeCliMcpConfigurator-->>ClaudeCliMcpConfigurator: client.SetStatus(Error, mismatch message)
        else Transport matches
            ClaudeCliMcpConfigurator-->>ClaudeCliMcpConfigurator: client.SetStatus(Configured)
        end
    end
    ClaudeCliMcpConfigurator-->>BackgroundTask: Return McpStatus

    BackgroundTask-->>McpClientConfigSection: ContinueWith(result, faulted)
    McpClientConfigSection-->>McpClientConfigSection: statusRefreshInFlight.Remove(client)
    McpClientConfigSection-->>McpClientConfigSection: lastStatusChecks[client] = DateTime.UtcNow
    McpClientConfigSection-->>McpClientConfigSection: ApplyStatusToUi(showChecking=false)
    McpClientConfigSection-->>User: Updated non-blocking status indicator
Loading

Sequence diagram for non-blocking Claude CLI registration and status handling

sequenceDiagram
    actor User
    participant ClaudeCliMcpConfigurator
    participant ExecPath
    participant ClaudeCliCLI as ClaudeCLI
    participant MCPClient as McpClient
    participant McpClientConfigSection
    participant BackgroundTask

    User->>ClaudeCliMcpConfigurator: Register()

    ClaudeCliMcpConfigurator->>ExecPath: TryRun(claudePath, "mcp get UnityMCP")
    alt Existing registration found
        ExecPath-->>ClaudeCliMcpConfigurator: serverExists = true
        ClaudeCliMcpConfigurator->>ExecPath: TryRun(claudePath, "mcp remove UnityMCP")
        alt Remove fails
            ExecPath-->>ClaudeCliMcpConfigurator: Failure, log warning
        else Remove succeeds
            ExecPath-->>ClaudeCliMcpConfigurator: Removed
        end
    else No existing registration
        ExecPath-->>ClaudeCliMcpConfigurator: serverExists = false
    end

    ClaudeCliMcpConfigurator->>ExecPath: TryRun(claudePath, registerArgs)
    alt Register fails
        ExecPath-->>ClaudeCliMcpConfigurator: stdout, stderr
        ClaudeCliMcpConfigurator-->>User: Throw InvalidOperationException
    else Register succeeds
        ExecPath-->>ClaudeCliMcpConfigurator: Success
        ClaudeCliMcpConfigurator->>MCPClient: SetStatus(Configured)
        ClaudeCliMcpConfigurator-->>User: Return without blocking status check
    end

    User->>McpClientConfigSection: Trigger manual or automatic refresh
    McpClientConfigSection-->>McpClientConfigSection: Capture projectDir and useHttpTransport on main thread
    McpClientConfigSection->>BackgroundTask: Task.Run(CheckStatusWithProjectDir(projectDir, useHttpTransport))
    BackgroundTask->>ClaudeCliMcpConfigurator: CheckStatusWithProjectDir(projectDir, useHttpTransport)
    ClaudeCliMcpConfigurator->>ExecPath: TryRun(claudePath, "mcp list")
    alt UnityMCP present
        ClaudeCliMcpConfigurator->>ExecPath: TryRun(claudePath, "mcp get UnityMCP")
        ClaudeCliMcpConfigurator-->>ClaudeCliMcpConfigurator: Parse Type http or Type stdio
        alt Transport mismatch
            ClaudeCliMcpConfigurator->>MCPClient: SetStatus(Error, mismatch message)
        else Transport matches
            ClaudeCliMcpConfigurator->>MCPClient: SetStatus(Configured)
        end
    else UnityMCP absent
        ClaudeCliMcpConfigurator->>MCPClient: SetStatus(NotConfigured)
    end
    ClaudeCliMcpConfigurator-->>BackgroundTask: Return McpStatus
    BackgroundTask-->>McpClientConfigSection: Update status and UI asynchronously
Loading

Updated class diagram for Claude CLI configuration and transport-aware status flow

classDiagram
    class McpClient {
        +McpStatus status
        +SetStatus(status, message)
    }

    class McpClientConfiguratorBase {
        <<abstract>>
        +McpClient client
        +GetConfigPath() string
        +CheckStatus(attemptAutoRewrite bool) McpStatus
        +Register()
        +Unregister()
        +GetManualSnippet() string
    }

    class ClaudeCliMcpConfigurator {
        +ClaudeCliMcpConfigurator(McpClient client)
        +GetConfigPath() string
        +CheckStatus(attemptAutoRewrite bool) McpStatus
        +CheckStatusWithProjectDir(projectDir string, useHttpTransport bool?, attemptAutoRewrite bool) McpStatus
        -Register()
        -Unregister()
    }

    class IMcpClientConfigurator {
        <<interface>>
        +GetConfigPath() string
        +CheckStatus(attemptAutoRewrite bool) McpStatus
    }

    class McpClientConfigSection {
        -List~IMcpClientConfigurator~ configurators
        -Dictionary~IMcpClientConfigurator, DateTime~ lastStatusChecks
        -HashSet~IMcpClientConfigurator~ statusRefreshInFlight
        -int selectedClientIndex
        +RefreshSelectedClient(forceImmediate bool)
        +RefreshClientStatus(client IMcpClientConfigurator, forceImmediate bool)
        +RefreshClaudeCliStatus(client IMcpClientConfigurator, forceImmediate bool)
        +UpdateManualConfiguration()
        +UpdateClaudeCliPathVisibility()
    }

    class McpConnectionSection {
        -enum TransportProtocol
        +event Action OnManualConfigUpdateRequested
        +event Action OnTransportChanged
        +RegisterCallbacks()
    }

    class MCPForUnityEditorWindow {
        -McpConnectionSection connectionSection
        -McpClientConfigSection clientConfigSection
        +CreateGUI()
    }

    class EditorPrefKeys {
        <<static>>
        +UseHttpTransport string
    }

    McpClientConfiguratorBase <|-- ClaudeCliMcpConfigurator
    IMcpClientConfigurator <|.. ClaudeCliMcpConfigurator

    McpClientConfigSection --> IMcpClientConfigurator : manages
    McpClientConfigSection --> ClaudeCliMcpConfigurator : special handling

    ClaudeCliMcpConfigurator --> McpClient : updates status

    McpConnectionSection --> MCPForUnityEditorWindow : raises events
    MCPForUnityEditorWindow --> McpConnectionSection : owns
    MCPForUnityEditorWindow --> McpClientConfigSection : owns

    McpConnectionSection --> McpClientConfigSection : OnManualConfigUpdateRequested
    McpConnectionSection --> McpClientConfigSection : OnTransportChanged triggers RefreshSelectedClient(forceImmediate)

    McpClientConfigSection --> EditorPrefKeys : reads UseHttpTransport
Loading

File-Level Changes

Change Details Files
Make Claude CLI status checking transport-aware and safe for background-thread execution.
  • Refactored CheckStatus to delegate to a new internal CheckStatusWithProjectDir method that accepts projectDir and useHttpTransport parameters for thread-safe usage from background tasks.
  • Ensured projectDir is either passed in or computed via Application.dataPath only when not provided, keeping main-thread-only API calls out of background threads.
  • After confirming UnityMCP is registered via mcp list, added a second mcp get UnityMCP call and parse its output for Type: http or Type: stdio to detect the registered transport.
  • Introduced logic to compare the registered transport against the current useHttpTransport value and set an error status (with log warning) when there is a mismatch, instead of reporting Configured.
MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
Fix registration/unregistration flow to avoid UI blocking and stale transport registrations.
  • Changed the registration flow to always attempt mcp get UnityMCP first and, if present, run mcp remove UnityMCP before registering, logging a warning but continuing if removal fails.
  • Simplified mcp add error handling to throw on failure instead of treating already exists as a special case, since pre-removal guarantees a clean state.
  • After successful registration, immediately set client status to Configured and removed the synchronous CheckStatus() call to avoid blocking the Unity Editor UI.
  • Removed the blocking CheckStatus() call after Unregister(), relying on the explicit status update to NotConfigured.
MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
Update client configuration UI to perform asynchronous, thread-safe Claude CLI status refreshes and support forced immediate refresh.
  • Modified RefreshSelectedClient to accept a forceImmediate flag and only force synchronous status checks for non-Claude CLI configurators, while Claude CLI uses the async path even when forced.
  • Adjusted RefreshClaudeCliStatus so that forced refresh requests still go through the async path, and only schedule a new refresh when forced or when the cached status is stale.
  • Before starting the background Task, captured projectDir and useHttpTransport on the main thread using Application.dataPath and EditorPrefs, then passed them into the background thread.
  • Used the new CheckStatusWithProjectDir API for ClaudeCliMcpConfigurator inside the background task, falling back to MCPServiceLocator.Client.CheckClientStatus for other configurators.
MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
MCPForUnity/Editor/Constants/EditorPrefKeys.cs
MCPForUnity/Editor/Helpers/... (existing helper references)
Wire transport protocol changes in the connection UI to trigger immediate client status refresh.
  • Added an OnTransportChanged event to McpConnectionSection and invoked it whenever the transport protocol dropdown value changes, alongside existing manual-config updates.
  • Subscribed MCPForUnityEditorWindow to the new OnTransportChanged event to call clientConfigSection.RefreshSelectedClient(forceImmediate: true) so transport switches immediately revalidate Claude CLI registration.
MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs
MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs

Possibly linked issues

  • #(not provided): PR fixes HTTP↔stdio registration and transport mismatch detection, likely causing the Desktop stdio connection failure described.

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 Dec 30, 2025

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

Adds transport-aware status checks and registration cleanup for the Claude CLI configurator, exposes an OnTransportChanged event from the connection UI, and triggers an immediate client-config refresh when transport changes.

Changes

Cohort / File(s) Summary
Configurator: transport-aware status & registration
MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
Adds internal CheckStatusWithProjectDir(string projectDir, bool useHttpTransport, bool attemptAutoRewrite = true) used by ClaudeCliMcpConfigurator; runs thread-safe transport detection via mcp list / mcp get UnityMCP, compares registered transport (http vs stdio) to current setting, sets error or Configured status. Registration removes existing UnityMCP before re-registering and sets status to Configured immediately; unregister sets NotConfigured without re-check.
Client config UI: async refresh & API change
MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
RefreshSelectedClient() signature changed to RefreshSelectedClient(bool forceImmediate = false). Async Claude CLI status checks now capture main-thread-only values (projectDir, useHttpTransport) before Task.Run and call CheckStatusWithProjectDir. Removed an immediate-force fast-path in Claude CLI refresh logic.
Connection UI: transport event
MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs
Adds public OnTransportChanged event (Action) and invokes it after manual transport updates (alongside existing UI updates and logging).
Editor window: event-driven refresh
MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
Subscribes to OnTransportChanged and calls RefreshSelectedClient(forceImmediate: true) so transport changes immediately refresh selected client status.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant Conn as McpConnectionSection
    participant Editor as MCPForUnityEditorWindow
    participant ClientUI as McpClientConfigSection
    participant Config as ClaudeCliMcpConfigurator
    participant CLI as MCP CLI

    User->>Conn: Change transport setting
    Conn->>Conn: Update internal transport mode
    Conn->>Conn: Invoke OnTransportChanged
    Conn->>Editor: OnTransportChanged event

    rect rgb(230,245,255)
    Editor->>ClientUI: RefreshSelectedClient(forceImmediate:true)
    ClientUI->>ClientUI: Capture projectDir & useHttpTransport (main thread)
    ClientUI->>ClientUI: Start Task.Run for status check
    end

    rect rgb(240,250,240)
    ClientUI->>Config: CheckStatusWithProjectDir(projectDir, useHttpTransport)
    Config->>CLI: Run "mcp list"
    CLI-->>Config: list output
    alt UnityMCP present
        Config->>CLI: Run "mcp get UnityMCP"
        CLI-->>Config: config (contains Type: http|stdio)
        Config->>Config: Parse registered transport and compare
        alt mismatch
            Config->>ClientUI: Return error status + log warning
        else match
            Config->>ClientUI: Return Configured status
        end
    else Not registered
        Config->>ClientUI: Return NotConfigured
    end
    end

    ClientUI->>Editor: Display updated status
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I hopped through configs, sniffing streams and modes,

I nudged the transport bell and watched the status go.
Threads captured carrots, CLI commands in tow,
Registrations tidied, old crumbs swept from the row.
A rabbit cheers: refresh, respond — now off I go! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly addresses the main objectives: fixing Claude Code registration and HTTP transport issues, which are the core problems solved across the codebase modifications.
✨ 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 1 issue, and left some high level feedback:

  • The new CheckStatusWithProjectDir helper still falls back to EditorPrefs.GetBool when useHttpTransport is null, which will break if any future callers invoke it from a background thread without passing the captured value; consider either enforcing non-null useHttpTransport (e.g., make it non-nullable) or explicitly guarding that code path to require main-thread execution.
  • Now that background tasks call CheckStatusWithProjectDir directly, it might be worth centralizing all Claude CLI status checks (including those via MCPServiceLocator.Client.CheckClientStatus) through this method to avoid divergent code paths and ensure consistent transport mismatch handling.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `CheckStatusWithProjectDir` helper still falls back to `EditorPrefs.GetBool` when `useHttpTransport` is null, which will break if any future callers invoke it from a background thread without passing the captured value; consider either enforcing non-null `useHttpTransport` (e.g., make it non-nullable) or explicitly guarding that code path to require main-thread execution.
- Now that background tasks call `CheckStatusWithProjectDir` directly, it might be worth centralizing all Claude CLI status checks (including those via `MCPServiceLocator.Client.CheckClientStatus`) through this method to avoid divergent code paths and ensure consistent transport mismatch handling.

## Individual Comments

### Comment 1
<location> `MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs:343-346` </location>
<code_context>
+        }
+
+        // Internal version that accepts projectDir and useHttpTransport to allow calling from background threads
+        internal McpStatus CheckStatusWithProjectDir(string projectDir, bool? useHttpTransport, bool attemptAutoRewrite = true)
         {
             try
</code_context>

<issue_to_address>
**issue (bug_risk):** Access to EditorPrefs from this method can occur on a background thread when useHttpTransport is null.

Because this overload may be called from background threads and still falls back to `EditorPrefs.GetBool` when `useHttpTransport` is null, there’s a latent threading hazard if future callers invoke it off the main thread. Please either (a) enforce that `useHttpTransport` is non-null for any off-main-thread usage (with guards/asserts), or (b) refactor so that all `EditorPrefs` access is done in a clearly main-thread-only helper.
</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

🧹 Nitpick comments (2)
MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs (1)

356-360: Verify thread safety: Application.dataPath is main-thread only.

The fallback to Application.dataPath when projectDir is null will throw if called from a background thread. The current design expects callers to always provide projectDir when calling from background threads.

Since CheckStatus() (the public entry point) calls this with null, it must only be called from the main thread. The internal CheckStatusWithProjectDir is called with captured values from McpClientConfigSection.RefreshClaudeCliStatus, which correctly captures projectDir before Task.Run. This looks correct, but the implicit contract should be documented.

Consider adding a comment or assertion to make the threading contract explicit:

 // Use provided projectDir or get it from Application.dataPath (main thread only)
 if (string.IsNullOrEmpty(projectDir))
 {
+    // This path is only safe from the main thread
     projectDir = Path.GetDirectoryName(Application.dataPath);
 }
MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs (1)

347-355: Defensive branch for non-Claude clients is unreachable but harmless.

The else branch (lines 352-355) will never execute because RefreshClaudeCliStatus is only called when client is ClaudeCliMcpConfigurator (see line 307). The defensive check is harmless but adds unnecessary code.

Consider removing the unreachable branch for clarity:

🔎 Proposed simplification
 Task.Run(() =>
 {
-    // For Claude CLI configurator, use thread-safe version with captured values
-    if (client is ClaudeCliMcpConfigurator claudeConfigurator)
-    {
-        claudeConfigurator.CheckStatusWithProjectDir(projectDir, useHttpTransport, attemptAutoRewrite: false);
-    }
-    else
-    {
-        MCPServiceLocator.Client.CheckClientStatus(client, attemptAutoRewrite: false);
-    }
+    // Use thread-safe version with captured main-thread values
+    ((ClaudeCliMcpConfigurator)client).CheckStatusWithProjectDir(projectDir, useHttpTransport, attemptAutoRewrite: false);
 }).ContinueWith(t =>
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b866a4c and 5122d8b.

📒 Files selected for processing (4)
  • MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
  • MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
  • MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs
  • MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-12-29T15:23:11.613Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 491
File: MCPForUnity/Editor/Windows/EditorPrefs/EditorPrefsWindow.cs:78-115
Timestamp: 2025-12-29T15:23:11.613Z
Learning: In MCPForUnity, prefer relying on the established testing process to catch UI initialization issues instead of adding defensive null checks for UI elements in editor windows. This means during reviews, verify that tests cover UI initialization paths and that code avoids repetitive null-check boilerplate in Editor Windows. If a UI element can be null, ensure there is a well-tested fallback or that its initialization is guaranteed by design, rather than sprinkling null checks throughout editor code.

Applied to files:

  • MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
  • MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs
  • MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
  • MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
📚 Learning: 2025-09-03T16:00:55.839Z
Learnt from: dsarno
Repo: CoplayDev/unity-mcp PR: 0
File: :0-0
Timestamp: 2025-09-03T16:00:55.839Z
Learning: ComponentResolver in UnityMcpBridge/Editor/Tools/ManageGameObject.cs is a nested static class within ManageGameObject, not a sibling type. The `using static MCPForUnity.Editor.Tools.ManageGameObject;` import is required to access ComponentResolver methods directly without the outer class qualifier.

Applied to files:

  • MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
📚 Learning: 2025-10-13T13:27:23.040Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 316
File: TestProjects/UnityMCPTests/Assets/Tests/EditMode/Resources.meta:1-8
Timestamp: 2025-10-13T13:27:23.040Z
Learning: UnityMcpBridge is a legacy project kept for backwards compatibility; MCPForUnity is the only active Unity plugin project. GUID collisions between UnityMcpBridge and MCPForUnity are acceptable.

Applied to files:

  • MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
  • MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
📚 Learning: 2025-09-04T01:01:11.927Z
Learnt from: dsarno
Repo: CoplayDev/unity-mcp PR: 260
File: UnityMcpBridge/UnityMcpServer~/src/server_version.txt:1-1
Timestamp: 2025-09-04T01:01:11.927Z
Learning: The UnityMcpBridge project is not maintaining changelogs yet, so don't suggest adding changelog entries for version bumps.

Applied to files:

  • MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
📚 Learning: 2025-09-05T16:22:04.960Z
Learnt from: dsarno
Repo: CoplayDev/unity-mcp PR: 265
File: README.md:204-204
Timestamp: 2025-09-05T16:22:04.960Z
Learning: In the Unity MCP project, the ServerInstaller.cs creates a symlink from ~/Library/AppSupport to ~/Library/Application Support on macOS to mitigate argument parsing and quoting issues in some MCP clients. The README documentation should use the shortened AppSupport path, not the full "Application Support" path with spaces.

Applied to files:

  • MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
🧬 Code graph analysis (3)
MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs (1)
MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs (1)
  • RefreshSelectedClient (292-303)
MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs (4)
MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs (2)
  • ClaudeCliMcpConfigurator (328-602)
  • ClaudeCliMcpConfigurator (330-330)
MCPForUnity/Editor/Constants/EditorPrefKeys.cs (1)
  • EditorPrefKeys (7-49)
MCPForUnity/Editor/Services/ClientConfigurationService.cs (1)
  • CheckClientStatus (51-56)
MCPForUnity/Editor/Services/IClientConfigurationService.cs (1)
  • CheckClientStatus (30-30)
MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs (5)
MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs (1)
  • McpStatus (30-30)
MCPForUnity/Editor/Helpers/ExecPath.cs (2)
  • ExecPath (12-272)
  • TryRun (161-227)
MCPForUnity/Editor/Constants/EditorPrefKeys.cs (1)
  • EditorPrefKeys (7-49)
MCPForUnity/Editor/Models/McpClient.cs (1)
  • SetStatus (42-54)
MCPForUnity/Editor/Helpers/McpLog.cs (4)
  • Error (48-51)
  • McpLog (7-52)
  • Warn (43-46)
  • Info (37-41)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Sourcery review
  • GitHub Check: Sourcery review
🔇 Additional comments (11)
MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs (5)

337-340: LGTM: Clean delegation to thread-safe internal method.

The refactoring separates the public interface from the implementation that supports background thread calls. This maintains backward compatibility while enabling the new async status check flow.


363-370: Thread safety concern: Application.platform accessed unconditionally.

Application.platform is accessed without a null-check for projectDir, meaning this code path runs regardless of whether we're on a background thread. Unlike Application.dataPath, Application.platform is thread-safe in Unity (it's a static readonly value cached at startup), so this is actually safe.


384-416: Transport mismatch detection logic is correct and well-structured.

The transport validation:

  1. Runs mcp list to check if UnityMCP exists
  2. Runs mcp get UnityMCP to retrieve registration details
  3. Parses output for "Type: http" or "Type: stdio" (case-insensitive)
  4. Compares against current preference and sets error status on mismatch

The logic correctly handles the case where neither http nor stdio is detected (falls through to Configured status).


489-512: LGTM: Clean registration flow with proper cleanup.

The registration logic:

  1. Checks for existing registration and removes it first
  2. Logs the cleanup action for debugging
  3. Handles removal failure gracefully (warns but continues)
  4. Sets status immediately after successful registration

This addresses the stale registration issue and avoids blocking CheckStatus calls.


553-555: LGTM: Status is set directly, avoiding blocking call.

The comment clarifies the intent - status is already set, so no additional CheckStatus call is needed.

MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs (2)

58-58: LGTM: New event follows established pattern.

The OnTransportChanged event follows the same Action pattern as the existing OnManualConfigUpdateRequested event on line 57.


119-119: LGTM: Event invocation placed correctly in the callback flow.

The OnTransportChanged event is invoked after OnManualConfigUpdateRequested, which ensures the manual configuration is updated before the client config section refreshes with the new transport setting. This ordering prevents stale data in the refresh.

MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs (1)

178-179: LGTM: Transport change triggers immediate client refresh.

The subscription correctly uses forceImmediate: true to bypass the 45-second refresh debounce, ensuring the UI immediately reflects the transport change. The null-conditional operator safely handles the case where clientConfigSection might not be initialized.

Note: The event subscription is not explicitly unsubscribed in OnDisable. However, since connectionSection is an instance field that gets recreated in CreateGUI, and guiCreated is reset to false in OnDisable, the subscription lifetime is tied to the window's UI lifecycle, which appears acceptable for this Unity editor window pattern.

MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs (3)

9-9: LGTM: Necessary import for EditorPrefKeys constant.

The import is required for accessing EditorPrefKeys.UseHttpTransport on line 343.


292-303: LGTM: Well-designed forceImmediate parameter.

The logic forceImmediate || client is not ClaudeCliMcpConfigurator ensures:

  • Non-Claude clients always refresh immediately (synchronous and fast)
  • Claude CLI clients only force immediate when explicitly requested (e.g., transport change)

This addresses the PR objective of immediate status updates on transport changes while preserving the async behavior for Claude CLI during normal operation.


336-356: LGTM: Thread-safe status check implementation.

The implementation correctly:

  1. Guards against concurrent refreshes with statusRefreshInFlight
  2. Captures main-thread-only values (Application.dataPath, EditorPrefs) before Task.Run
  3. Routes Claude CLI to the thread-safe CheckStatusWithProjectDir method
  4. Falls back to standard CheckClientStatus for other clients (defensive, though this branch shouldn't be reached given the routing in RefreshClientStatus)

Address code review feedback by making CheckStatusWithProjectDir thread-safe
by design rather than by convention:

1. Made projectDir and useHttpTransport parameters non-nullable to prevent
   accidental background thread calls without captured values

2. Removed nullable fallback to EditorPrefs.GetBool() which would cause
   thread safety violations if called from background threads

3. Added ArgumentNullException for null projectDir instead of falling back
   to Application.dataPath (which is main-thread only)

4. Added XML documentation clearly stating threading contracts:
   - CheckStatus() must be called from main thread
   - CheckStatusWithProjectDir() is safe for background threads

5. Removed unreachable else branch in async status check code

These changes make it impossible to misuse the API from background threads,
with compile-time enforcement instead of runtime errors.
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 (1)
MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs (1)

373-393: Remove Application.platform access from thread-safe method or pass platform as parameter.

The method CheckStatusWithProjectDir is documented to be called from background threads, yet it directly accesses Application.platform on lines 374 and 378. Unity's documentation explicitly states that the Unity API is not thread-safe and should only be accessed from the main thread. To maintain thread-safety, capture the platform value in the main-thread caller (CheckStatus) and pass it as a parameter to CheckStatusWithProjectDir, similar to how projectDir and useHttpTransport are already handled.

🧹 Nitpick comments (1)
MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs (1)

395-427: Transport mismatch detection relies on CLI output format.

The parsing of "Type: http" and "Type: stdio" strings (lines 409-410) assumes a specific human-readable output format from claude mcp get. This is consistent with the PR objective to parse human-readable output rather than JSON. However, CLI output formats can change between versions.

Consider adding a fallback or logging when neither transport type is detected, to handle potential format changes gracefully:

🔎 Suggested defensive handling
 bool registeredWithHttp = getStdout.Contains("Type: http", StringComparison.OrdinalIgnoreCase);
 bool registeredWithStdio = getStdout.Contains("Type: stdio", StringComparison.OrdinalIgnoreCase);
+
+// If neither transport detected, log and assume configured (graceful degradation)
+if (!registeredWithHttp && !registeredWithStdio)
+{
+    McpLog.Warn($"Could not determine transport type from Claude CLI output. Assuming configured.");
+    client.SetStatus(McpStatus.Configured);
+    return client.status;
+}
 
 // Check for transport mismatch
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5122d8b and 0413c45.

📒 Files selected for processing (2)
  • MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
  • MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-09-03T16:00:55.839Z
Learnt from: dsarno
Repo: CoplayDev/unity-mcp PR: 0
File: :0-0
Timestamp: 2025-09-03T16:00:55.839Z
Learning: ComponentResolver in UnityMcpBridge/Editor/Tools/ManageGameObject.cs is a nested static class within ManageGameObject, not a sibling type. The `using static MCPForUnity.Editor.Tools.ManageGameObject;` import is required to access ComponentResolver methods directly without the outer class qualifier.

Applied to files:

  • MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
📚 Learning: 2025-10-13T13:27:23.040Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 316
File: TestProjects/UnityMCPTests/Assets/Tests/EditMode/Resources.meta:1-8
Timestamp: 2025-10-13T13:27:23.040Z
Learning: UnityMcpBridge is a legacy project kept for backwards compatibility; MCPForUnity is the only active Unity plugin project. GUID collisions between UnityMcpBridge and MCPForUnity are acceptable.

Applied to files:

  • MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
📚 Learning: 2025-12-29T15:23:11.613Z
Learnt from: msanatan
Repo: CoplayDev/unity-mcp PR: 491
File: MCPForUnity/Editor/Windows/EditorPrefs/EditorPrefsWindow.cs:78-115
Timestamp: 2025-12-29T15:23:11.613Z
Learning: In MCPForUnity, prefer relying on the established testing process to catch UI initialization issues instead of adding defensive null checks for UI elements in editor windows. This means during reviews, verify that tests cover UI initialization paths and that code avoids repetitive null-check boilerplate in Editor Windows. If a UI element can be null, ensure there is a well-tested fallback or that its initialization is guaranteed by design, rather than sprinkling null checks throughout editor code.

Applied to files:

  • MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
  • MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
🧬 Code graph analysis (2)
MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs (2)
MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs (2)
  • ClaudeCliMcpConfigurator (328-613)
  • ClaudeCliMcpConfigurator (330-330)
MCPForUnity/Editor/Constants/EditorPrefKeys.cs (1)
  • EditorPrefKeys (7-49)
MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs (5)
MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs (1)
  • McpStatus (30-30)
MCPForUnity/Editor/Constants/EditorPrefKeys.cs (1)
  • EditorPrefKeys (7-49)
MCPForUnity/Editor/Helpers/ExecPath.cs (2)
  • ExecPath (12-272)
  • TryRun (161-227)
MCPForUnity/Editor/Models/McpClient.cs (1)
  • SetStatus (42-54)
MCPForUnity/Editor/Helpers/McpLog.cs (4)
  • Error (48-51)
  • McpLog (7-52)
  • Warn (43-46)
  • Info (37-41)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (6)
MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs (2)

292-303: LGTM - API extension for forced refresh.

The forceImmediate parameter enables transport-change-triggered refreshes. The logic correctly forces immediate refresh for non-Claude CLI clients (which are fast synchronous checks) while respecting the parameter for Claude CLI.


336-350: Thread-safety fix looks correct.

Capturing Application.dataPath and EditorPrefs.GetBool() on the main thread before Task.Run properly addresses the threading hazard. The cast to ClaudeCliMcpConfigurator on line 349 is safe since this method is only called when the guard on line 307 passes.

One minor observation: consider whether the attemptAutoRewrite: false on line 350 is intentional for async checks. The main-thread CheckStatus() defaults to true, so background refreshes won't auto-rewrite configurations. This seems correct for status-only polling but worth confirming.

MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs (4)

337-347: Thread-safety design is well-structured.

The separation of concerns is clean: CheckStatus() handles main-thread API access and delegates to the thread-safe CheckStatusWithProjectDir(). The XML doc correctly documents the threading contract.


349-371: Thread-safe overload with proper contract enforcement.

The ArgumentNullException on line 370 codifies the thread-safety requirement at runtime, preventing misuse. The XML docs clearly state the threading contract.


500-522: Cleanup-before-register pattern ensures transport consistency.

The change to always remove existing registration before re-registering (lines 500-510) correctly addresses stale registration handling. The warning on line 508 appropriately continues despite removal failure. Setting status to Configured immediately (line 522) avoids the UI blocking issue.


564-566: Consistent with Register() pattern.

The comment clarifies the intentional removal of the blocking CheckStatus() call, matching the pattern established in Register().

@dsarno
Copy link
Collaborator Author

dsarno commented Dec 31, 2025

Closing, it's included in #499

@dsarno dsarno closed this Dec 31, 2025
@dsarno dsarno deleted the fix-http-editorpref branch January 2, 2026 19:19
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