Skip to content

Comments

fix: Claude Code registration, thread-safety, and auto-detect beta server (#664)#667

Merged
dsarno merged 8 commits intoCoplayDev:betafrom
dsarno:fix/claude-code-multi-scope-cleanup-v2
Feb 2, 2026
Merged

fix: Claude Code registration, thread-safety, and auto-detect beta server (#664)#667
dsarno merged 8 commits intoCoplayDev:betafrom
dsarno:fix/claude-code-multi-scope-cleanup-v2

Conversation

@dsarno
Copy link
Collaborator

@dsarno dsarno commented Feb 2, 2026

Description

Fixes issues with Claude Code MCP client registration causing connection errors due to stale configs at different scopes, thread-safety errors during status checks, and UX issues with client selection.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)

Changes Made

Multi-scope config cleanup

  • Add RemoveFromAllScopes helper to remove configs from local/user/project scopes before registering
  • Use explicit --scope local when registering to prevent scope ambiguity
  • Handle legacy 'unityMCP' naming in all scopes
  • Update manual snippets to show multi-scope cleanup commands

Thread-safe status check

  • Fix "GetString can only be called from main thread" errors in background status checks
  • Capture Application.platform, HttpEndpointUtility.IsRemoteScope(), and AssetPathUtility.GetMcpServerPackageSource() on main thread before Task.Run()
  • Pass all main-thread-only values as parameters to CheckStatusWithProjectDir()

UX improvements

  • Persist last selected client in EditorPrefs so it restores on window reopen
  • Prevents hiding config issues by always defaulting to first client alphabetically

Auto-detect beta package for UseBetaServer

  • Add IsPreReleaseVersion() helper to detect -beta, -alpha, -rc, -preview, -pre suffixes in package version
  • UseBetaServer now defaults to true only for prerelease package versions
  • Main branch users get false default, beta branch users get true default automatically
  • Use EditorConfigurationCache consistently for UseBetaServer access
  • Fix CodexConfigHelperTests to explicitly set UseBetaServer for deterministic tests

Beta mode status validation fix

  • Fix "Incorrect Path" status shown when beta mode is enabled
  • Status check now uses same package source logic as registration (mcpforunityserver>=0.0.0a0 for beta)
  • Fixed both main-thread (CheckStatus) and background-thread (McpClientConfigSection) code paths

EditorPrefs Manager improvements

  • Show "Unset. Default: [value]" for EditorPrefs that haven't been explicitly set
  • Disable value field and save button for unset items (grayed out at 60% opacity)
  • Fix search filter bug where reloading window showed filtered results with empty search field

Workflow updates for beta/main version management

  • Update beta-release.yml to set Unity package version with semver beta suffix (e.g., 9.4.0-beta.1)
  • Update release.yml to merge beta -> main before version bump
  • Update release.yml to strip beta suffix from version before stable release

Code cleanup

  • Remove dead Outbound class, _outbox BlockingCollection, and writer thread (nothing ever enqueued)
  • Remove verbose IO success logs, keep only failure logs

Version Flow

Beta branch (automatic on push to beta):
  package.json: 9.3.1 -> 9.4.0-beta.1  (detected as prerelease -> UseBetaServer=true)
  pyproject.toml: stays 9.3.1 until PyPI publish -> 9.4.0b{timestamp}

Release workflow (manual dispatch on main):
  1. Merges beta -> main
  2. Strips suffix: 9.4.0-beta.1 -> 9.4.0
  3. Bumps: 9.4.0 -> 9.4.1 (or minor/major per input)
  4. Tags and publishes to PyPI/Docker/GitHub Release

Testing/Screenshots/Recordings

  • Verified Claude Code registration works with explicit local scope
  • Verified status check no longer throws thread errors
  • Verified client dropdown persists selection across window close/reopen
  • Verified IsPreReleaseVersion() correctly detects beta versions
  • Verified Claude Code/Desktop both show "Configured" status with beta mode enabled
  • Verified EditorPrefs Manager correctly shows unset keys vs explicit values

Documentation Updates

  • I have added/removed/modified tools or resources
  • If yes, I have updated all documentation files using:
    • The LLM prompt at tools/UPDATE_DOCS_PROMPT.md (recommended)
    • Manual updates following the guide at tools/UPDATE_DOCS.md

Note: Only manual configuration snippets updated (showing multi-scope cleanup). No tool/resource API changes.

Related Issues

Fixes #664

Additional Notes

The original issue reported WebSocket disconnect errors on Windows. This PR addresses:

  1. Configuration conflicts that can cause connection issues
  2. Thread-safety bug discovered during investigation
  3. UX improvement to persist client selection
  4. Automatic beta server detection so beta users get matching server versions without manual configuration
  5. Status validation that correctly handles beta mode package sources
  6. EditorPrefs Manager clarity for unset vs default values

dsarno and others added 4 commits February 1, 2026 08:16
Updated the Git URL for adding the package to include the branch name.
…fig conflicts (CoplayDev#664)

- Add RemoveFromAllScopes helper to remove from local/user/project scopes
- Use explicit --scope local when registering
- Update manual snippets to show multi-scope cleanup
- Handle legacy 'unityMCP' naming in all scopes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The background thread status check was accessing main-thread-only Unity
APIs (Application.platform, EditorPrefs via HttpEndpointUtility and
AssetPathUtility), causing "GetString can only be called from main thread"
errors.

Now all main-thread-only values are captured before Task.Run() and passed
as parameters to CheckStatusWithProjectDir().

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remember last selected client in EditorPrefs so it restores on window
  reopen (prevents hiding config issues by defaulting to first client)
- Remove dead Outbound class, _outbox BlockingCollection, and writer
  thread that was never used (nothing ever enqueued to outbox)
- Keep only failure IO logs, remove verbose success logging

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Feb 2, 2026

Reviewer's Guide

Refactors Claude CLI status checking to be fully thread-safe by capturing main-thread-only values up front, tightens Claude MCP registration/unregistration to use local scope and clear all scopes to avoid stale configs, simplifies stdio transport logging, and adds UX improvements like persisting the last selected client plus a small README URL fix.

Sequence diagram for thread-safe Claude CLI status check with captured main-thread values

sequenceDiagram
    actor User
    participant UnityEditor as UnityEditorWindow
    participant McpClientConfigSection
    participant ClaudeCliMcpConfigurator
    participant BackgroundTask as Task_CheckStatus

    User->>UnityEditor: Open MCP Client Config window
    UnityEditor->>McpClientConfigSection: InitializeUI()
    McpClientConfigSection->>McpClientConfigSection: RestoreSelectedClient()

    User->>UnityEditor: Click Refresh Status
    UnityEditor->>McpClientConfigSection: RefreshClaudeCliStatus(client, forceImmediate)

    rect rgb(235,235,255)
        note over McpClientConfigSection: Capture main-thread-only values
        McpClientConfigSection->>McpClientConfigSection: projectDir = Path.GetDirectoryName(Application.dataPath)
        McpClientConfigSection->>McpClientConfigSection: useHttpTransport = EditorConfigurationCache.Instance.UseHttpTransport
        McpClientConfigSection->>McpClientConfigSection: claudePath = MCPServiceLocator.Paths.GetClaudeCliPath()
        McpClientConfigSection->>McpClientConfigSection: platform = Application.platform
        McpClientConfigSection->>McpClientConfigSection: isRemoteScope = HttpEndpointUtility.IsRemoteScope()
        McpClientConfigSection->>McpClientConfigSection: expectedPackageSource = AssetPathUtility.GetMcpServerPackageSource()
    end

    McpClientConfigSection->>BackgroundTask: Task.Run(CheckStatusWithProjectDir(...captured values..., attemptAutoRewrite=false))

    activate BackgroundTask
    BackgroundTask->>ClaudeCliMcpConfigurator: CheckStatusWithProjectDir(projectDir, useHttpTransport, claudePath, platform, isRemoteScope, expectedPackageSource, attemptAutoRewrite=false)

    ClaudeCliMcpConfigurator->>ClaudeCliMcpConfigurator: Determine pathPrepend from platform
    ClaudeCliMcpConfigurator->>ClaudeCliMcpConfigurator: Run claude mcp list / status
    alt HTTP transport
        ClaudeCliMcpConfigurator->>ClaudeCliMcpConfigurator: configuredTransport = isRemoteScope ? HttpRemote : Http
    else STDIO transport
        ClaudeCliMcpConfigurator->>ClaudeCliMcpConfigurator: ExtractPackageSourceFromCliOutput(stdout)
        ClaudeCliMcpConfigurator->>ClaudeCliMcpConfigurator: Compare with expectedPackageSource
    end
    ClaudeCliMcpConfigurator-->>BackgroundTask: McpStatus

    BackgroundTask-->>McpClientConfigSection: ContinueWith(update UI on main thread)
    deactivate BackgroundTask

    McpClientConfigSection->>McpClientConfigSection: UpdateClientStatus()
    McpClientConfigSection->>UnityEditor: Refresh status label and controls
Loading

Class diagram for updated MCP client configurator, config UI, and stdio host

classDiagram
    class McpClientConfiguratorBase {
        +McpStatus CheckStatus(bool attemptAutoRewrite)
        +string GetManualSnippet()
        +void Register()
        +void Unregister()
        -void RegisterWithCapturedValues(string projectDir, string claudePath, bool useHttpTransport, Models.ConfiguredTransport serverTransport, string httpUrl, string uvxPath, string gitUrl, string packageName, string pathPrepend, bool shouldForceRefresh)
        -void UnregisterWithCapturedValues(string projectDir, string claudePath, string pathPrepend)
        -static void RemoveFromAllScopes(string claudePath, string projectDir, string pathPrepend)
        -string SanitizeShellHeaderValue(string value)
        -string ExtractPackageSourceFromCliOutput(string stdout)
        -void ValidateClaudeCli()
        -void Configure()
        -void ConfigureInternal()
        -void ApplyConfiguration()
        -void ValidateProjectDir(string projectDir)
        -void UpdateClientStatus(McpStatus status)
        -void LogStatusChange(McpStatus status)
        -void HandleConfigurationError(Exception ex)
        -void SetConfiguredTransport(Models.ConfiguredTransport transport)
        -void UpdateEditorPrefs()
        -void RefreshClient()
        -void ShowStatusNotification(McpStatus status)
        -void ShowErrorDialog(string message)
        -void ShowInfoDialog(string message)
        -void ShowWarningDialog(string message)
        -void LogInfo(string message)
        -void LogWarning(string message)
        -void LogError(string message)
        -void EnsureClaudeCliPath()
        -void EnsureProjectDir()
        -void EnsureTransportMode()
        -void EnsureHttpEndpoint()
        -void EnsureStdioEndpoint()
        -void EnsureApiKey()
        -void EnsureUvxPath()
        -void EnsureGitUrl()
        -void EnsurePackageName()
        -void EnsurePathPrepend(RuntimePlatform platform)
        -void EnsureExpectedPackageSource()
        -bool IsRegistrationRequired()
        -bool IsUnregistrationRequired()
        -bool ShouldForceRefresh()
        -bool IsRemoteScope()
        -bool IsHttpTransportEnabled()
        -bool IsStdioTransportEnabled()
        -bool IsConfigured()
        -bool IsError()
        -bool IsNotConfigured()
        -bool IsConfigOutOfDate()
        -bool IsConfigMismatch()
        -bool IsTransportMismatch()
        -bool IsPackageVersionMismatch()
        -bool HasValidClaudeCliPath()
        -bool HasValidProjectDir()
        -bool HasValidTransportMode()
        -bool HasValidHttpEndpoint()
        -bool HasValidStdioEndpoint()
        -bool HasValidApiKey()
        -bool HasValidUvxPath()
        -bool HasValidGitUrl()
        -bool HasValidPackageName()
        -bool HasValidPathPrepend()
        -bool HasValidExpectedPackageSource()
        -bool TryParseStatusFromOutput(string stdout)
        -bool TryExtractTransportInfo(string stdout)
        -bool TryExtractPackageInfo(string stdout)
        -bool TryExtractScopeInfo(string stdout)
        -bool TryExtractErrorInfo(string stderr)
        -bool TryRunClaudeCli(string args, string projectDir, out string stdout, out string stderr, int timeoutMs, string pathPrepend)
        -bool TryRunRegistration(string args, string projectDir, out string stdout, out string stderr, int timeoutMs, string pathPrepend)
        -bool TryRunUnregistration(string args, string projectDir, out string stdout, out string stderr, int timeoutMs, string pathPrepend)
        -bool TryRunList(string projectDir, out string stdout, out string stderr, int timeoutMs, string pathPrepend)
        -McpStatus CheckStatusWithProjectDir(string projectDir, bool useHttpTransport, string claudePath, RuntimePlatform platform, bool isRemoteScope, string expectedPackageSource, bool attemptAutoRewrite)
    }

    class McpClientConfigSection {
        -List~IMcpClientConfigurator~ configurators
        -int selectedClientIndex
        -DropdownField clientDropdown
        -VisualElement root
        -Label statusLabel
        -Button registerButton
        -Button unregisterButton
        -Button refreshStatusButton
        -VisualElement claudeCliPathRow
        -TextField claudeCliPathField
        -Toggle useHttpTransportToggle
        -Label manualConfigLabel
        -TextField manualConfigField
        -Button copyManualConfigButton
        -Label httpEndpointLabel
        -Label stdioEndpointLabel
        -Label transportModeLabel
        -Label apiKeyLabel
        -Label uvxPathLabel
        -Label gitUrlLabel
        -Label packageNameLabel
        -Label pathPrependLabel
        -Label expectedPackageSourceLabel
        -Label logOutputLabel
        -ScrollView logScrollView
        -Button openLogsButton
        -Button clearLogsButton
        -Button helpButton
        -Button docsButton
        -Button reportIssueButton
        -Button openSettingsButton
        -Button resetConfigButton
        -Button openProjectFolderButton
        -Button openClaudeCliFolderButton
        -Button openUnityPreferencesButton
        -Button openPackageManagerButton
        -void InitializeUI()
        -void RegisterCallbacks()
        -void UpdateClientStatus()
        -void UpdateManualConfiguration()
        -void UpdateClaudeCliPathVisibility()
        -void RefreshClaudeCliStatus(IMcpClientConfigurator client, bool forceImmediate)
        -void RefreshUiFromConfig()
        -void OnTransportModeChanged()
        -void OnApiKeyChanged()
        -void OnClaudeCliPathChanged()
        -void OnRegisterClicked()
        -void OnUnregisterClicked()
        -void OnRefreshStatusClicked()
        -void OnHelpClicked()
        -void OnDocsClicked()
        -void OnReportIssueClicked()
        -void OnOpenSettingsClicked()
        -void OnResetConfigClicked()
        -void OnOpenProjectFolderClicked()
        -void OnOpenClaudeCliFolderClicked()
        -void OnOpenUnityPreferencesClicked()
        -void OnOpenPackageManagerClicked()
        -void AppendLog(string message)
        -void ClearLog()
        -void LoadConfigurators()
        -void SaveSelectedClient()
        -void RestoreSelectedClient()
    }

    class StdioBridgeHost {
        <<static>>
        -int mainThreadId
        -object startStopLock
        -object clientsLock
        -HashSet~TcpClient~ activeClients
        -CancellationTokenSource cts
        -Task listenerTask
        -int processingCommands
        -int pendingCommands
        -int nextCommandId
        -Dictionary~int,QueuedCommand~ pendingCommandMap
        -Dictionary~int,QueuedCommand~ inFlightCommands
        -Dictionary~string,QueuedCommand~ notificationHandlers
        -IPEndPoint listenEndpoint
        -TcpListener tcpListener
        -bool isShuttingDown
        -bool allowBatchMode
        -ulong MaxFrameBytes
        -int FrameIOTimeoutMs
        -void Start()
        -void Stop()
        -void StopInternal()
        -void StartListenerLoop()
        -Task ListenAsync(CancellationToken token)
        -Task HandleClientAsync(TcpClient client, CancellationToken token)
        -Task<string> ReadFrameAsync(NetworkStream stream)
        -Task WriteFrameAsync(NetworkStream stream, byte[] payload)
        -void IoInfo(string message)
        -bool IsDebugEnabled()
        -void LogDebug(string message)
        -void LogError(string message)
        -void LogInfo(string message)
        -void OnCommandReceived(string json)
        -void OnNotificationReceived(string json)
        -void OnResponseReceived(string json)
        -void DispatchCommand(QueuedCommand cmd)
        -void CompleteCommand(int id, string response)
        -void FailCommand(int id, string error)
        -void RegisterNotificationHandler(string method, QueuedCommand handler)
        -void UnregisterNotificationHandler(string method)
        -bool FolderExists(string path)
    }

    McpClientConfigSection --> McpClientConfiguratorBase : uses
    StdioBridgeHost --> McpClientConfiguratorBase : transports for
Loading

File-Level Changes

Change Details Files
Make Claude CLI status checks fully thread-safe by capturing main-thread-only values and passing them into the worker method.
  • Extend CheckStatusWithProjectDir signature to take RuntimePlatform, remote-scope flag, and expected package source as parameters.
  • Capture Application.platform, HttpEndpointUtility.IsRemoteScope, and AssetPathUtility.GetMcpServerPackageSource on the main thread before launching background tasks.
  • Use the injected platform instead of Application.platform when choosing PATH prepends and the injected remote-scope flag and expected package source when resolving configured transport and version mismatches.
  • Add warning log when CheckStatusWithProjectDir throws and normalize error handling of configuredTransport.
MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
Harden Claude MCP registration/unregistration to use local scope and aggressively clean up registrations from all scopes, including legacy names.
  • Add a RemoveFromAllScopes helper that removes UnityMCP/unityMCP from local, user, and project scopes via the Claude CLI.
  • Change registration commands (http and stdio, both captured and direct) to always pass --scope local to avoid user-level config conflicts.
  • Replace ad-hoc mcp remove UnityMCP/unityMCP calls with RemoveFromAllScopes in register/unregister flows.
  • Update manual CLI snippets to show scoped add/remove commands and explicitly remove from multiple scopes for cleanup.
MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
Simplify stdio bridge host IO path by removing unused outbound queue and detailed write tracing.
  • Remove the Outbound helper class, BlockingCollection-based writer thread, and IO sequence tracking.
  • Strip per-write timing/sequence logging around response frame writes, keeping only high-level logging and WriteFrameAsync call.
MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs
Improve MCP client configuration UX by persisting and restoring the last selected client in the editor window.
  • Add a new EditorPrefs key to store the last selected client ID.
  • On UI initialization, restore the dropdown index from the stored client ID if available; otherwise default to the first client.
  • On client selection changes, save the selected configurator ID back to EditorPrefs.
MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
MCPForUnity/Editor/Constants/EditorPrefKeys.cs
Minor documentation tweak to pin the default Unity MCP package URL to the main branch.
  • Update README install snippet URL to append #main to the MCPForUnity path-based Git URL.
README.md

Possibly linked issues


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 Feb 2, 2026

📝 Walkthrough

Walkthrough

Thread-safety and scope-awareness added to Claude CLI status checks and registration/unregistration flows; cross-scope cleanup introduced; UI persists last-selected client; stdio transport per-frame IO logging and writer thread removed; beta/prerelease handling centralized and CI release workflows updated.

Changes

Cohort / File(s) Summary
Claude CLI configurator & status flow
MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs, MCPForUnity/Editor/Clients/ClaudeCliMcpConfigurator.cs
CheckStatus now delegates to an extended CheckStatusWithProjectDir that accepts captured platform, isRemoteScope, and expectedPackageSource; added RemoveFromAllScopes helper to clean registrations across local/user/project scopes; registrations use --scope local.
Editor UI: client selection & status calls
MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs, MCPForUnity/Editor/Constants/EditorPrefKeys.cs
Added LastSelectedClientId key; restore/persist selected client via EditorPrefs; capture main-thread platform/scope/packageSource and pass into background status checks.
Stdio transport I/O simplification
MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs
Removed Outbound type, _outbox writer thread, _ioSeq, and per-frame sequence-based IO logging while keeping framing and write operations.
Beta/prerelease helpers & config cache
MCPForUnity/Editor/Helpers/AssetPathUtility.cs, MCPForUnity/Editor/Services/EditorConfigurationCache.cs, MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.cs, MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
Centralized UseBetaServer via EditorConfigurationCache; added thread-safe overloads for beta-server arg generation, GetBetaServerFromArgsList, and IsPreReleaseVersion; UI reads from cache.
CI / release workflows
.github/workflows/beta-release.yml, .github/workflows/release.yml
Added job to compute/update Unity package beta version and conditional push to beta; adapted publish prerelease dependency; added merge-from-beta and strip-prerelease steps to derive stable versions.
Docs & package URL
README.md
Pinned Unity package installation URL to the main branch for MCPForUnity.
Tests
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/CodexConfigHelperTests.cs, Server/tests/test_transport_characterization.py
Tests now set/restore UseBetaServer during execution; transport tests patched to prevent legacy discovery fallback when PluginHub unavailable.

Sequence Diagram(s)

sequenceDiagram
    participant UI as "Editor UI"
    participant FS as "Project Dir / EditorPrefs"
    participant Config as "ClaudeCliMcpConfigurator"
    participant CLI as "Claude CLI / Package Manager"

    UI->>FS: capture platform, isRemoteScope, expectedPackageSource
    UI->>Config: CheckStatus(projectDir, useHttp, claudePath, platform, isRemoteScope, expectedPackageSource)
    Config->>CLI: run CLI status check (background thread) with captured args
    CLI-->>Config: status/result or exception
    Config-->>UI: marshal and update status on main thread
    UI->>FS: persist LastSelectedClientId (on selection change)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through threads and captured state,
Scoped the paths and cleaned the slate,
I saved the client you left behind,
Quieted logs, made frames aligned,
A tiny carrot for tidy slate — 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the three main categories of fixes: Claude Code registration, thread-safety, and auto-detect beta server.
Description check ✅ Passed The description includes all required template sections (Type of Change, Changes Made, Testing, Documentation Updates, Related Issues) with detailed explanations of the multi-scope cleanup, thread-safe status checks, UX improvements, beta server auto-detection, and workflow updates.
Linked Issues check ✅ Passed The code changes directly address all objectives from issue #664: eliminating stale configs across scopes, making registration explicit with --scope local, capturing main-thread values before background tasks, persisting client selection, and auto-detecting prerelease versions.
Out of Scope Changes check ✅ Passed All changes align with the stated objectives. The GitHub Actions workflow updates, helper method additions, EditorPrefs integration, thread-safety refactoring, and dead code removal are all directly related to fixing the reported issue and improving client registration reliability.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@dsarno dsarno changed the title Fix/claude code multi scope cleanup v2 fix: Claude Code registration and status check improvements (#664) Feb 2, 2026
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 left some high level feedback:

  • The manual snippets for unregistering (GetManualSnippet) now show claude mcp remove for UnityMCP only, but the code in RemoveFromAllScopes also cleans up the legacy unityMCP name; consider updating the snippet or adding a note so users can manually remove legacy registrations as well.
  • The scope strings (local, user, project) are hard-coded both in RemoveFromAllScopes and in the manual snippet; centralizing these as shared constants or a single helper will reduce the chance of future drift or typos between behavior and documentation.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The manual snippets for unregistering (`GetManualSnippet`) now show `claude mcp remove` for `UnityMCP` only, but the code in `RemoveFromAllScopes` also cleans up the legacy `unityMCP` name; consider updating the snippet or adding a note so users can manually remove legacy registrations as well.
- The scope strings (`local`, `user`, `project`) are hard-coded both in `RemoveFromAllScopes` and in the manual snippet; centralizing these as shared constants or a single helper will reduce the chance of future drift or typos between behavior and documentation.

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.

…ates

- Add IsPreReleaseVersion() helper to detect beta/alpha/rc package versions
- UseBetaServer now defaults to true only for prerelease package versions
- Main branch users get false default, beta branch users get true default
- Update beta-release.yml to set Unity package version with -beta.1 suffix
- Update release.yml to merge beta → main and strip beta suffix
- Fix CodexConfigHelperTests to explicitly set UseBetaServer for determinism
- Use EditorConfigurationCache consistently for UseBetaServer access

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@dsarno dsarno changed the title fix: Claude Code registration and status check improvements (#664) fix: Claude Code registration, thread-safety, and auto-detect beta server (#664) Feb 2, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@MCPForUnity/Editor/Helpers/AssetPathUtility.cs`:
- Around line 265-266: GetBetaServerFromArgs() currently reads
EditorConfigurationCache.Instance.UseBetaServer from a background thread which
may trigger EditorConfigurationCache.Refresh() and call EditorPrefs.GetBool()
(main-thread-only). Change the call site to capture useBetaServer on the main
thread (like expectedPackageSource in CheckStatusWithProjectDir()) and modify
GetBetaServerFromArgs() signature (or its helper) to accept a bool useBetaServer
parameter, then use that passed value instead of accessing
EditorConfigurationCache.Instance inside the background task; alternatively
ensure EditorConfigurationCache is initialized on the main thread before
spawning the Task.Run().
🧹 Nitpick comments (3)
MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.cs (1)

186-191: Consider using the cache setter for consistency.

While the reads at lines 141 and 295 now use EditorConfigurationCache.Instance.UseBetaServer, the write in the toggle callback still writes directly to EditorPrefs:

EditorPrefs.SetBool(EditorPrefKeys.UseBetaServer, evt.newValue);

This bypasses EditorConfigurationCache.Instance.SetUseBetaServer(value), which would update both the cache and EditorPrefs atomically. Until the cache is refreshed or the key is invalidated, subsequent reads from the cache may return stale values.

♻️ Suggested refactor to use the cache setter
 useBetaServerToggle.RegisterValueChangedCallback(evt =>
 {
-    EditorPrefs.SetBool(EditorPrefKeys.UseBetaServer, evt.newValue);
+    EditorConfigurationCache.Instance.SetUseBetaServer(evt.newValue);
     OnHttpServerCommandUpdateRequested?.Invoke();
     OnBetaModeChanged?.Invoke(evt.newValue);
 });
.github/workflows/beta-release.yml (1)

30-57: Add semver validation before arithmetic.
If BASE_VERSION isn’t X.Y.Z, the minor bump can break; consider validating like the PyPI step.

♻️ Suggested guard
           BASE_VERSION=$(echo "$CURRENT_VERSION" | sed -E 's/-[a-zA-Z]+\.[0-9]+$//')
+          if ! [[ "$BASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+            echo "Error: Could not parse version '$CURRENT_VERSION' -> '$BASE_VERSION'" >&2
+            exit 1
+          fi
.github/workflows/release.yml (1)

66-84: Guard against non-semver after stripping prerelease.
If the suffix isn’t matched, the next version bump can fail; consider validating STABLE_VERSION and failing fast.

♻️ Suggested guard
             STABLE_VERSION=$(echo "$CURRENT_VERSION" | sed -E 's/-[a-zA-Z]+\.[0-9]+$//')
+            if ! [[ "$STABLE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+              echo "Error: Could not parse version '$CURRENT_VERSION' -> '$STABLE_VERSION'" >&2
+              exit 1
+            fi

- Add thread-safe overloads for GetBetaServerFromArgs/List that accept
  pre-captured useBetaServer and gitUrlOverride parameters
- Use EditorConfigurationCache.SetUseBetaServer() in McpAdvancedSection
  for atomic cache + EditorPrefs update
- Add semver validation guard in beta-release.yml before version arithmetic
- Add semver validation guard in release.yml after stripping prerelease suffix

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
MCPForUnity/Editor/Helpers/AssetPathUtility.cs (1)

288-295: ⚠️ Potential issue | 🟠 Major

Thread-safe overload contract violated: parameterized versions still call unsafe main-thread-only APIs.

The parameterized GetBetaServerFromArgs(useBetaServer, gitUrlOverride, quoteFromPath) and GetBetaServerFromArgsList(useBetaServer, gitUrlOverride) overloads are designed to accept pre-captured values, suggesting they are safe for off-main-thread usage. However, both still call GetMcpServerPackageSource() at lines 289 and 342, which is unsafe: it reads EditorPrefs (main-thread only) and calls GetPackageVersion()PackageInfo.FindForAssembly() (UnityEditor API, main-thread only).

If these parameterized overloads are invoked from a background thread, they will crash with "can only be called from the main thread" exceptions.

Fix: Add string packageSource parameter to the parameterized overloads, pre-capture GetMcpServerPackageSource() in the non-parameterized wrappers on the main thread, and pass it through. This ensures all unsafe calls remain on the main thread.

🔧 Proposed fix (introduce pre-captured packageSource)
- public static string GetBetaServerFromArgs(bool quoteFromPath = false)
+ public static string GetBetaServerFromArgs(bool quoteFromPath = false)
  {
      // Read values from cache/EditorPrefs on main thread
      bool useBetaServer = Services.EditorConfigurationCache.Instance.UseBetaServer;
      string gitUrlOverride = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");
+     string packageSource = GetMcpServerPackageSource();
-     return GetBetaServerFromArgs(useBetaServer, gitUrlOverride, quoteFromPath);
+     return GetBetaServerFromArgs(useBetaServer, gitUrlOverride, packageSource, quoteFromPath);
  }

- public static string GetBetaServerFromArgs(bool useBetaServer, string gitUrlOverride, bool quoteFromPath = false)
+ public static string GetBetaServerFromArgs(bool useBetaServer, string gitUrlOverride, string packageSource, bool quoteFromPath = false)
  {
      // Explicit override (local path, git URL, etc.) always wins
      if (!string.IsNullOrEmpty(gitUrlOverride))
      {
          return $"--from {gitUrlOverride}";
      }

      // Beta server mode: use prerelease from PyPI
      if (useBetaServer)
      {
          string fromValue = quoteFromPath ? "\"mcpforunityserver>=0.0.0a0\"" : "mcpforunityserver>=0.0.0a0";
          return $"--prerelease explicit --from {fromValue}";
      }

      // Standard mode: use pinned version from package.json
-     string fromUrl = GetMcpServerPackageSource();
+     string fromUrl = packageSource;
      if (!string.IsNullOrEmpty(fromUrl))
      {
          return $"--from {fromUrl}";
      }

      return string.Empty;
  }

- public static System.Collections.Generic.IList<string> GetBetaServerFromArgsList()
+ public static System.Collections.Generic.IList<string> GetBetaServerFromArgsList()
  {
      // Read values from cache/EditorPrefs on main thread
      bool useBetaServer = Services.EditorConfigurationCache.Instance.UseBetaServer;
      string gitUrlOverride = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");
+     string packageSource = GetMcpServerPackageSource();
-     return GetBetaServerFromArgsList(useBetaServer, gitUrlOverride);
+     return GetBetaServerFromArgsList(useBetaServer, gitUrlOverride, packageSource);
  }

- public static System.Collections.Generic.IList<string> GetBetaServerFromArgsList(bool useBetaServer, string gitUrlOverride)
+ public static System.Collections.Generic.IList<string> GetBetaServerFromArgsList(bool useBetaServer, string gitUrlOverride, string packageSource)
  {
      var args = new System.Collections.Generic.List<string>();

      // Explicit override (local path, git URL, etc.) always wins
      if (!string.IsNullOrEmpty(gitUrlOverride))
      {
          args.Add("--from");
          args.Add(gitUrlOverride);
          return args;
      }

      // Beta server mode: use prerelease from PyPI
      if (useBetaServer)
      {
          args.Add("--prerelease");
          args.Add("explicit");
          args.Add("--from");
          args.Add("mcpforunityserver>=0.0.0a0");
          return args;
      }

      // Standard mode: use pinned version from package.json
-     string fromUrl = GetMcpServerPackageSource();
+     string fromUrl = packageSource;
      if (!string.IsNullOrEmpty(fromUrl))
      {
          args.Add("--from");
          args.Add(fromUrl);
      }

      return args;
  }

Also applies to: 342–347

🤖 Fix all issues with AI agents
In `@MCPForUnity/Editor/Helpers/AssetPathUtility.cs`:
- Around line 273-277: The explicit override branch returns an unquoted path
which breaks for local paths with spaces; update the branch that checks
gitUrlOverride to pass gitUrlOverride through the existing quoteFromPath helper
(i.e., use quoteFromPath(gitUrlOverride) when building the "--from" argument) so
the generated command string is correctly quoted; locate the gitUrlOverride
check in the same method and replace the raw interpolation with the quoted form
using the quoteFromPath function.

dsarno and others added 2 commits February 2, 2026 15:00
- Add packageSource parameter to thread-safe overloads to avoid calling
  GetMcpServerPackageSource() (which uses EditorPrefs) from background threads
- Apply quoteFromPath logic to gitUrlOverride and packageSource paths to
  handle local paths with spaces correctly

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…Unity discovery

The auto-select tests were failing because they only patched PluginHub
but not the fallback legacy connection pool discovery. When PluginHub
returns no results, the middleware falls back to discovering instances
via get_unity_connection_pool(), which found the real running Unity.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@dsarno dsarno merged commit cb08b0c into CoplayDev:beta Feb 2, 2026
2 checks passed
urun4m0r1 pushed a commit to urun4m0r1/unity-mcp that referenced this pull request Feb 6, 2026
- Remove ExclusiveAddressUse=false to prevent duplicate listeners
  binding to the same port after Domain Reload
- Increase port binding retry from 3x75ms to 10x200ms to allow
  old socket cleanup during Domain Reload

Based on beta branch (post-PR CoplayDev#682) which includes:
- CheckStatus speed-up (read ~/.claude.json directly)
- Thread-safety fixes (PR CoplayDev#667)
- Beta mode validation (PR CoplayDev#671)

Fixes CoplayDev#664
Related: CoplayDev#643

Co-Authored-By: Claude <noreply@anthropic.com>
urun4m0r1 pushed a commit to urun4m0r1/unity-mcp that referenced this pull request Feb 6, 2026
- Remove ExclusiveAddressUse=false to prevent duplicate listeners
  binding to the same port after Domain Reload
- Increase port binding retry from 3x75ms to 10x200ms to allow
  old socket cleanup during Domain Reload

Based on beta branch (post-PR CoplayDev#682) which includes:
- CheckStatus speed-up (read ~/.claude.json directly)
- Thread-safety fixes (PR CoplayDev#667)
- Beta mode validation (PR CoplayDev#671)

Fixes CoplayDev#664
Related: CoplayDev#643

Co-Authored-By: Claude <noreply@anthropic.com>
urun4m0r1 pushed a commit to urun4m0r1/unity-mcp that referenced this pull request Feb 7, 2026
- Remove ExclusiveAddressUse=false to prevent duplicate listeners
  binding to the same port after Domain Reload
- Increase port binding retry from 3x75ms to 10x200ms to allow
  old socket cleanup during Domain Reload

Based on beta branch (post-PR CoplayDev#682) which includes:
- CheckStatus speed-up (read ~/.claude.json directly)
- Thread-safety fixes (PR CoplayDev#667)
- Beta mode validation (PR CoplayDev#671)

Fixes CoplayDev#664
Related: CoplayDev#643

Co-Authored-By: Claude <noreply@anthropic.com>
urun4m0r1 pushed a commit to urun4m0r1/unity-mcp that referenced this pull request Feb 7, 2026
- Remove ExclusiveAddressUse=false to prevent duplicate listeners
  binding to the same port after Domain Reload
- Increase port binding retry from 3x75ms to 10x200ms to allow
  old socket cleanup during Domain Reload

Based on beta branch (post-PR CoplayDev#682) which includes:
- CheckStatus speed-up (read ~/.claude.json directly)
- Thread-safety fixes (PR CoplayDev#667)
- Beta mode validation (PR CoplayDev#671)

Fixes CoplayDev#664
Related: CoplayDev#643

Co-Authored-By: Claude <noreply@anthropic.com>
msanatan pushed a commit to msanatan/unity-mcp that referenced this pull request Feb 9, 2026
…rver (CoplayDev#664) (CoplayDev#667)

* Fix Git URL in README for package installation

Updated the Git URL for adding the package to include the branch name.

* fix: Clean up Claude Code config from all scopes to prevent stale config conflicts (CoplayDev#664)

- Add RemoveFromAllScopes helper to remove from local/user/project scopes
- Use explicit --scope local when registering
- Update manual snippets to show multi-scope cleanup
- Handle legacy 'unityMCP' naming in all scopes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Make Claude Code status check thread-safe (CoplayDev#664)

The background thread status check was accessing main-thread-only Unity
APIs (Application.platform, EditorPrefs via HttpEndpointUtility and
AssetPathUtility), causing "GetString can only be called from main thread"
errors.

Now all main-thread-only values are captured before Task.Run() and passed
as parameters to CheckStatusWithProjectDir().

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Persist client dropdown selection and remove dead IO code

- Remember last selected client in EditorPrefs so it restores on window
  reopen (prevents hiding config issues by defaulting to first client)
- Remove dead Outbound class, _outbox BlockingCollection, and writer
  thread that was never used (nothing ever enqueued to outbox)
- Keep only failure IO logs, remove verbose success logging

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: Auto-detect beta package to enable UseBetaServer + workflow updates

- Add IsPreReleaseVersion() helper to detect beta/alpha/rc package versions
- UseBetaServer now defaults to true only for prerelease package versions
- Main branch users get false default, beta branch users get true default
- Update beta-release.yml to set Unity package version with -beta.1 suffix
- Update release.yml to merge beta → main and strip beta suffix
- Fix CodexConfigHelperTests to explicitly set UseBetaServer for determinism
- Use EditorConfigurationCache consistently for UseBetaServer access

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Address code review feedback for thread-safety and validation

- Add thread-safe overloads for GetBetaServerFromArgs/List that accept
  pre-captured useBetaServer and gitUrlOverride parameters
- Use EditorConfigurationCache.SetUseBetaServer() in McpAdvancedSection
  for atomic cache + EditorPrefs update
- Add semver validation guard in beta-release.yml before version arithmetic
- Add semver validation guard in release.yml after stripping prerelease suffix

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Complete thread-safety for GetBetaServerFromArgs overloads

- Add packageSource parameter to thread-safe overloads to avoid calling
  GetMcpServerPackageSource() (which uses EditorPrefs) from background threads
- Apply quoteFromPath logic to gitUrlOverride and packageSource paths to
  handle local paths with spaces correctly

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Patch legacy connection pool in transport tests to prevent real Unity discovery

The auto-select tests were failing because they only patched PluginHub
but not the fallback legacy connection pool discovery. When PluginHub
returns no results, the middleware falls back to discovering instances
via get_unity_connection_pool(), which found the real running Unity.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
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.

[Bug] v.9.3.1 + Claude Code Connection Error

1 participant