Skip to content

Conversation

@whatevertogo
Copy link
Contributor

@whatevertogo whatevertogo commented Feb 11, 2026

Fixes #709 - Disabled Tools Reappear and Still Show Up in Client Lists

Description

Fixes #709 - Disabled tools now persist across Unity Editor restarts and are correctly filtered from client tool
listings.

Type of Change

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

Changes Made

Unity Editor (C#)

  • ToolDiscoveryService.cs: Added EnsurePreferenceInitialized() method to initialize EditorPrefs for tools on first
    discovery
  • McpToolsSection.cs: Added ReregisterToolsAsync() call to trigger tool reregistration when toggles change
  • WebSocketTransportClient.cs: Implemented ReregisterToolsAsync() to send updated tool list to server
  • IMcpTransportClient.cs: Added ReregisterToolsAsync() interface method

MCP Server (Python)

  • unity_instance_middleware.py: Added on_list_tools() middleware method to filter tools based on Unity session
    state
  • tool_registry.py: Added unity_target parameter to @mcp_for_unity_tool decorator for explicit tool visibility
    control
  • Tool filtering applies only in HTTP/WebSocket transport mode when PluginHub is configured
  • Tool aliases (e.g., create_script) follow their target tool's enabled state via _tool_alias_to_unity_target
    mapping

Testing/Screenshots/Recordings

  • Added 60+ characterization tests in test_transport_characterization.py
  • Added 7 tool preference persistence tests in ToolDiscoveryServiceTests.cs
  • Added user-scoped tool lookup tests in test_custom_tool_service_user_scope.py

Documentation Updates

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

Related Issues

Additional Notes

Limitations:

  • Tool filtering does not apply in stdio transport mode (TCP protocol limitation). All tools are shown as before.
  • HTTP/WebSocket transport mode: Tool filtering works correctly; disabled tools do not appear in client listings.

Behavior:

  • Server-only tools (set_active_instance, debug_request_context, manage_script_capabilities,
    execute_custom_tool) always remain visible
  • Unknown tools are kept visible for forward compatibility
  • Script helper tools follow their target tool's enabled state

Summary by Sourcery

Preserve Unity tool enablement state across sessions and ensure MCP server only lists tools that are enabled for the current Unity context and user.

Bug Fixes:

  • Prevent disabled Unity tools and their aliases from reappearing in MCP client tool listings when they are not enabled in the current project or user context.
  • Ensure built-in tools with auto-registration disabled remain disabled when users turn them off, even after rediscovery or editor restarts.
  • Avoid hiding tools when PluginHub metadata or session resolution fails, when the active Unity instance is stale, or when no registry metadata is available.

Enhancements:

  • Introduce Unity-aware tool visibility metadata and filtering in the server middleware, including support for server-only tools, alias tools, and per-Unity-session tool unions.
  • Add a unity_target parameter to the MCP tool registration decorator to explicitly control Unity visibility, and mark core server utilities and script helpers with appropriate targets.
  • Thread user_id through PluginHub lookups, custom tool services, and execution paths to support user-scoped tool registration in remote-hosted deployments.
  • Expose a non-creating transport client getter and implement tool re-registration over WebSocket so client-side tool toggle changes are pushed to the server.

Tests:

  • Add extensive server-side tests covering tool filtering behavior, user-scoped PluginHub lookups, and tool registry metadata handling.
  • Add Unity editor tests to verify persistence and behavior of tool enablement preferences across service instances and with built-in tools.

Summary by CodeRabbit

  • New Features

    • Transport API to reregister tools plus editor UI support for single and batched re-registration.
    • Tool registration metadata enhanced to enable explicit Unity targeting; listings respect active Unity project and user-scoped resolution.
  • Bug Fixes

    • Tool discovery no longer overrides a previously disabled preference for built-in tools.
  • Tests

    • Expanded coverage for discovery, preference persistence, transport filtering, user-scoped resolution, and tool-registry metadata.

whatevertogo and others added 3 commits February 11, 2026 22:04
Fixes CoplayDev#709

- Remove logic in ToolDiscoveryService.EnsurePreferenceInitialized
  that forced built-in tools to be re-enabled on every Unity launch
- Add ReregisterToolsAsync() to IMcpTransportClient for dynamic tool
  list updates when toggles change in the UI
- Implement tool reregistration in WebSocketTransportClient
- Add ReregisterToolsAsync() to McpToolsSection to trigger updates
  when tool toggles are flipped
- Add unit tests for ToolDiscoveryService preference persistence

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 11, 2026 15:23
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Feb 11, 2026

Reviewer's Guide

Implements persistent Unity tool enablement preferences and server-side filtering of disabled tools from HTTP/WebSocket client tool listings, including alias/visibility metadata and user-scoped tool resolution, plus editor-side support to re-register tools over WebSocket and extensive characterization tests.

Sequence diagram for Unity tool toggle and WebSocket tool reregistration

sequenceDiagram
    actor UnityUser
    participant McpToolsSection
    participant ToolDiscoveryService
    participant TransportManager
    participant HttpClient as WebSocketTransportClient
    participant McpServer

    UnityUser->>McpToolsSection: Toggle tool checkbox
    McpToolsSection->>ToolDiscoveryService: SetToolEnabled(toolName, enabled)
    ToolDiscoveryService-->>McpToolsSection: Preference stored in EditorPrefs
    McpToolsSection->>McpToolsSection: HandleToggleChange(tool, enabled, updateSummary, reregisterTools)
    McpToolsSection->>McpToolsSection: UpdateSummary()
    alt reregisterTools == true
        McpToolsSection->>TransportManager: GetClient(TransportMode.Http)
        TransportManager-->>McpToolsSection: HttpClient (if created)
        alt HttpClient is null or not connected
            McpToolsSection-->>McpToolsSection: Skip reregistration
        else HttpClient exists and IsConnected
            McpToolsSection->>HttpClient: ReregisterToolsAsync()
            activate HttpClient
            HttpClient->>HttpClient: SendRegisterToolsAsync(_lifecycleCts.Token)
            HttpClient->>McpServer: register_tools(tools payload)
            McpServer-->>HttpClient: Registration acknowledged
            HttpClient-->>McpToolsSection: Reregistration completed
            deactivate HttpClient
        end
    end
Loading

Sequence diagram for HTTP list_tools with Unity-aware filtering

sequenceDiagram
    actor Client
    participant HttpTransport as HTTPTransport
    participant UnityMiddleware as UnityInstanceMiddleware
    participant FastMCP as FastMCPCore
    participant PluginHub
    participant Registry as ToolRegistry

    Client->>HttpTransport: list_tools
    HttpTransport->>UnityMiddleware: on_list_tools(context)
    UnityMiddleware->>UnityMiddleware: _inject_unity_instance(context)
    UnityMiddleware->>FastMCP: call_next(context) (list_tools)
    FastMCP-->>UnityMiddleware: tools (all registered)

    UnityMiddleware->>UnityMiddleware: _should_filter_tool_listing()
    alt transport != http or PluginHub not configured
        UnityMiddleware-->>Client: tools (unfiltered)
    else Filtering enabled
        UnityMiddleware->>UnityMiddleware: _refresh_tool_visibility_metadata_from_registry()
        UnityMiddleware->>Registry: get_registered_tools()
        Registry-->>UnityMiddleware: registry_tools
        UnityMiddleware->>UnityMiddleware: Build
        UnityMiddleware->>UnityMiddleware: _unity_managed_tool_names
        UnityMiddleware->>UnityMiddleware: _tool_alias_to_unity_target
        UnityMiddleware->>UnityMiddleware: _server_only_tool_names

        UnityMiddleware->>PluginHub: get_sessions(user_id)
        PluginHub-->>UnityMiddleware: sessions
        UnityMiddleware->>UnityMiddleware: _resolve_candidate_project_hashes(unity_instance)
        UnityMiddleware->>PluginHub: get_tools_for_project(project_hash, user_id)
        PluginHub-->>UnityMiddleware: registered_tools
        UnityMiddleware->>UnityMiddleware: Collect enabled_tool_names

        loop For each tool in tools
            UnityMiddleware->>UnityMiddleware: _is_tool_visible(tool.name, enabled_tool_names)
        end
        UnityMiddleware-->>Client: filtered tools
    end
Loading

Sequence diagram for execute_custom_tool with user-scoped resolution

sequenceDiagram
    actor Client
    participant FastMCP as FastMCPCore
    participant ExecuteTool as execute_custom_tool
    participant CustomToolService
    participant PluginHub
    participant UnityInstance as UnityInstanceMiddleware

    Client->>FastMCP: execute_custom_tool(tool_name, parameters)
    FastMCP->>ExecuteTool: execute_custom_tool(ctx, tool_name, parameters)
    ExecuteTool->>ExecuteTool: get_user_id_from_context(ctx)
    ExecuteTool->>CustomToolService: execute_tool(project_id, tool_name, unity_instance, params, user_id)
    activate CustomToolService
    CustomToolService->>CustomToolService: get_tool_definition(project_id, tool_name, user_id)
    alt tool in _project_tools
        CustomToolService-->>CustomToolService: Use local definition
    else not local
        CustomToolService->>PluginHub: get_tool_definition(project_id, tool_name, user_id)
        PluginHub-->>CustomToolService: definition
    end
    CustomToolService->>UnityInstance: send_with_unity_instance(..., user_id)
    UnityInstance-->>CustomToolService: initial MCPResponse
    alt requires_polling
        CustomToolService->>CustomToolService: _poll_until_complete(..., user_id)
        loop until complete or timeout
            CustomToolService->>UnityInstance: send_with_unity_instance(..., user_id)
            UnityInstance-->>CustomToolService: MCPResponse
        end
    end
    CustomToolService-->>ExecuteTool: final MCPResponse
    deactivate CustomToolService
    ExecuteTool-->>Client: MCPResponse
Loading

Class diagram for Unity transport clients, tool UI, and registry metadata

classDiagram
    class McpToolsSection {
        -Dictionary~string, Toggle~ toolToggleMap
        -IEnumerable~ToolMetadata~ allTools
        +CreateToolRow(tool ToolMetadata) VisualElement
        -HandleToggleChange(tool ToolMetadata, enabled bool, updateSummary bool, reregisterTools bool) void
        -ReregisterToolsAsync() void
        -SetAllToolsState(enabled bool) void
        -UpdateSummary() void
    }

    class ToolDiscoveryService {
        +IsToolEnabled(toolName string) bool
        +SetToolEnabled(toolName string, enabled bool) void
        -EnsurePreferenceInitialized(metadata ToolMetadata) void
    }

    class TransportManager {
        -IMcpTransportClient _httpClient
        -IMcpTransportClient _stdioClient
        +StartAsync(mode TransportMode) Task~bool~
        +StopAsync(mode TransportMode) Task
        +VerifyAsync(mode TransportMode) Task~bool~
        +GetState(mode TransportMode) TransportState
        +IsRunning(mode TransportMode) bool
        +GetClient(mode TransportMode) IMcpTransportClient
    }

    class IMcpTransportClient {
        <<interface>>
        +bool IsConnected
        +Task~bool~ StartAsync()
        +Task StopAsync()
        +Task~bool~ VerifyAsync()
        +Task ReregisterToolsAsync()
    }

    class WebSocketTransportClient {
        +bool IsConnected
        -CancellationTokenSource _lifecycleCts
        +Task~bool~ StartAsync()
        +Task StopAsync()
        +Task~bool~ VerifyAsync()
        +Task ReregisterToolsAsync()
        -Task SendRegisterToolsAsync(token CancellationToken)
    }

    class StdioTransportClient {
        +bool IsConnected
        +Task~bool~ StartAsync()
        +Task StopAsync()
        +Task~bool~ VerifyAsync()
        +Task ReregisterToolsAsync()
    }

    McpToolsSection --> ToolDiscoveryService : uses
    McpToolsSection --> TransportManager : uses
    TransportManager --> IMcpTransportClient : returns
    WebSocketTransportClient ..|> IMcpTransportClient
    StdioTransportClient ..|> IMcpTransportClient

    class UnityInstanceMiddleware {
        -dict~string, string~ _active_by_key
        -RLock _lock
        -RLock _metadata_lock
        -set~string~ _unity_managed_tool_names
        -dict~string, string~ _tool_alias_to_unity_target
        -set~string~ _server_only_tool_names
        -tuple~tuple~string, string~~ _tool_visibility_signature
        -float _last_tool_visibility_refresh
        -float _tool_visibility_refresh_interval_seconds
        -bool _has_logged_empty_registry_warning
        +async on_list_tools(context MiddlewareContext) Any
        -_should_filter_tool_listing() bool
        -async _resolve_enabled_tool_names_for_context(context MiddlewareContext) set~string~ or None
        -_refresh_tool_visibility_metadata_from_registry() void
        -_resolve_candidate_project_hashes(active_instance str or None) list~str~
        -_is_tool_visible(tool_name str or None, enabled_tool_names set~str~) bool
    }

    class ToolRegistryDecorator {
        +mcp_for_unity_tool(name str or None, description str or None, unity_target str or None, **kwargs) Callable
    }

    class RegisteredToolMetadata {
        +func Callable
        +name str
        +description str or None
        +unity_target str or None
        +kwargs dict
    }

    UnityInstanceMiddleware --> RegisteredToolMetadata : reads

    class PluginHub {
        <<static>>
        +is_configured() bool
        +async get_sessions(user_id str or None) SessionList
        +async get_tools_for_project(project_hash str, user_id str or None) list~Any~
        +async get_tool_definition(project_hash str, tool_name str, user_id str or None) Any or None
    }

    UnityInstanceMiddleware --> PluginHub : queries sessions and tools

    class CustomToolService {
        -dict~str, dict~str, ToolDefinitionModel~~ _project_tools
        +async list_registered_tools(project_id str, user_id str or None) list~ToolDefinitionModel~
        +async get_tool_definition(project_id str, tool_name str, user_id str or None) ToolDefinitionModel or None
        +async execute_tool(project_id str, tool_name str, unity_instance str or None, params dict~str, object~ or None, user_id str or None) MCPResponse
        -async _poll_until_complete(project_id str, unity_instance str or None, tool_name str, initial_params dict~str, object~, initial_response Any, poll_action str, user_id str or None) MCPResponse
    }

    CustomToolService --> PluginHub : resolves tools per user

    class ToolFunctions {
        +execute_custom_tool
        +manage_script
        +create_script
        +delete_script
        +validate_script
        +find_in_file
        +script_apply_edits
        +manage_script_capabilities
        +set_active_instance
        +debug_request_context
    }

    ToolFunctions --> ToolRegistryDecorator : decorated by mcp_for_unity_tool

    class UnityTargetMetadata {
        +manage_script : base tool
        +aliases : create_script, delete_script, validate_script, find_in_file, script_apply_edits
        +server_only : execute_custom_tool, manage_script_capabilities, set_active_instance, debug_request_context
    }

    UnityInstanceMiddleware --> UnityTargetMetadata : derives visibility from
Loading

File-Level Changes

Change Details Files
Preserve Unity tool enabled/disabled state in EditorPrefs without being overridden by discovery and add focused tests for preference behavior.
  • Simplified ToolDiscoveryService.EnsurePreferenceInitialized so it only initializes the EditorPrefs key when missing and no longer forces built-in tools back to enabled
  • Added ToolDiscoveryServiceTests covering EditorPrefs writes, default behavior for unknown tools, preservation of stored values, cross-instance persistence, and ensuring built-in AutoRegister=false tools stay disabled when explicitly turned off
MCPForUnity/Editor/Services/ToolDiscoveryService.cs
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ToolDiscoveryServiceTests.cs
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ToolDiscoveryServiceTests.cs.meta
TestProjects/UnityMCPTests/Assets/Tests/Editor.meta
Allow the Unity editor UI to trigger (and batch) tool re-registration to the MCP server over HTTP/WebSocket.
  • Extended IMcpTransportClient with a ReregisterToolsAsync method and implemented it as a no-op for StdioTransportClient
  • Added ReregisterToolsAsync implementation in WebSocketTransportClient that reuses SendRegisterToolsAsync and logs success/failure, guarded by connection and lifecycle token checks
  • Promoted TransportManager.GetClient to a public method so UI code can access the HTTP client instance
  • Updated McpToolsSection to call ReregisterToolsAsync when individual tool toggles change and to batch bulk enable/disable operations so they result in a single re-registration
MCPForUnity/Editor/Services/Transport/IMcpTransportClient.cs
MCPForUnity/Editor/Services/Transport/TransportManager.cs
MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs
MCPForUnity/Editor/Services/Transport/Transports/StdioTransportClient.cs
MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs
Introduce server-side tool visibility metadata (unity_target) and middleware-based filtering of list_tools responses based on Unity session state and aliases.
  • Extended mcp_for_unity_tool decorator to accept a unity_target parameter, normalize its values, store it in the internal registry, and ensure it is not forwarded into mcp.tool kwargs
  • Annotated core tools (manage_script, create_script, delete_script, validate_script, find_in_file, script_apply_edits, manage_script_capabilities, set_active_instance, execute_custom_tool, debug_request_context) with appropriate unity_target values to distinguish Unity-managed, alias, and server-only tools
  • Added get_registered_tools-based visibility metadata computation in UnityInstanceMiddleware with cached sets of Unity-managed tools, server-only tools, and alias mappings, plus logging and throttling
  • Implemented UnityInstanceMiddleware.on_list_tools to conditionally filter tools only in HTTP mode when PluginHub is configured, resolving enabled tool names from PluginHub sessions and project hashes and preserving visibility for unknown or server-only tools
Server/src/services/registry/tool_registry.py
Server/src/services/registry/__init__.py
Server/src/transport/unity_instance_middleware.py
Server/src/services/tools/manage_script.py
Server/src/services/tools/find_in_file.py
Server/src/services/tools/script_apply_edits.py
Server/src/services/tools/manage_script_capabilities.py
Server/src/services/tools/set_active_instance.py
Server/src/services/tools/execute_custom_tool.py
Server/src/services/tools/debug_request_context.py
Server/tests/test_tool_registry_metadata.py
Server/tests/test_transport_characterization.py
Make PluginHub and custom tools user-scoped in remote-hosted HTTP mode so tool registrations and execution respect user_id.
  • Extended PluginHub.get_tools_for_project and get_tool_definition to accept an optional user_id and pass it through to the registry’s get_session_id_by_hash
  • Propagated user_id through CustomToolService.list_registered_tools, get_tool_definition, execute_tool, and its polling path so both definition lookup and Unity transport calls are user-scoped
  • Introduced get_user_id_from_context helper that reads user_id from FastMCP context when http_remote_hosted is enabled and used it in execute_custom_tool and the custom_tools resource so requests carry the user_id into CustomToolService
  • Added tests verifying user-scoped behavior for PluginHub project tools and tool definitions, CustomToolService’s threading of user_id, execute_custom_tool, and the custom_tools resource
Server/src/transport/plugin_hub.py
Server/src/services/custom_tool_service.py
Server/src/services/resources/custom_tools.py
Server/src/services/tools/execute_custom_tool.py
Server/tests/test_transport_characterization.py
Server/tests/test_custom_tool_service_user_scope.py
Expand transport characterization tests to cover tool-list filtering semantics for Unity-managed tools, aliases, server-only tools, error conditions, and multi-session scenarios.
  • Added helper _tool_registry_for_visibility_tests to describe a small registry of Unity-managed, alias, and server-only tools
  • Added multiple async tests for UnityInstanceMiddleware.on_list_tools covering filtering of disabled tools and aliases, behavior when the enabled set is empty, skipping filtering on PluginHub failures or stale hashes, using user-scoped lookups in remote-hosted mode, preserving results when the tool registry is empty, and computing unions across multiple sessions
  • Extended tests around PluginHub to assert user-scoped get_tools_for_project and get_tool_definition behaviors
Server/tests/test_transport_characterization.py

Assessment against linked issues

Issue Objective Addressed Explanation
#709 Ensure disabled MCP tools remain disabled across Unity Editor restarts (their enabled/disabled state is persisted correctly).
#709 Ensure disabled MCP tools do not appear in the client tool list (e.g., Claude Code or Trae) after connecting.

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 11, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Initializes tool preferences without forced overrides, adds transport-level ReregisterToolsAsync and UI batching to invoke it, exposes TransportManager.GetClient publicly, implements server-side HTTP tool-list filtering with user/session scoping, and extends unit and Unity Editor tests for prefs and user-scoped tool behavior.

Changes

Cohort / File(s) Summary
Preference Initialization
MCPForUnity/Editor/Services/ToolDiscoveryService.cs
Removed conditional branch that forced writes when built-in & not auto-registered; now only initializes missing preference keys with default (metadata.AutoRegister
Transport Interface & Implementations
MCPForUnity/Editor/Services/Transport/IMcpTransportClient.cs, MCPForUnity/Editor/Services/Transport/Transports/StdioTransportClient.cs, MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs
Added Task ReregisterToolsAsync() to IMcpTransportClient; stdio client implements it as a no-op; WebSocket client implements async re-registration with connection/cancellation checks and logging.
Transport Manager
MCPForUnity/Editor/Services/Transport/TransportManager.cs
Widened accessibility of GetClient(TransportMode) from private to public (same switch logic).
UI Tool Section
MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs
Added fire-and-forget ReregisterToolsAsync() helper; HandleToggleChange gains reregisterTools flag; bulk toggles batch state changes and perform a single re-registration.
Server-side Middleware
Server/src/transport/unity_instance_middleware.py
Added on_list_tools middleware that injects Unity context, resolves enabled tool names (project/user-scoped), and filters HTTP tool listings using alias/server-only/visibility rules with caching and robust fallbacks/logging.
PluginHub API
Server/src/transport/plugin_hub.py
get_tools_for_project and get_tool_definition updated to accept optional user_id and pass it to session resolution.
Custom Tool Service & Callers
Server/src/services/custom_tool_service.py, Server/src/services/resources/custom_tools.py, Server/src/services/tools/execute_custom_tool.py
Added get_user_id_from_context; propagated optional user_id through list/definition/execute/poll flows and updated callers to forward user-scoped parameter when available.
Tool Registry & Decorators
Server/src/services/registry/tool_registry.py, Server/src/services/tools/*.py
Added unity_target parameter to mcp_for_unity_tool, normalizing/validating values and storing normalized unity_target in registry; updated many server tool decorators to set explicit unity_target or None.
Server Tests
Server/tests/test_transport_characterization.py, Server/tests/test_custom_tool_service_user_scope.py, Server/tests/test_tool_registry_metadata.py
Added tests covering HTTP filtering behavior (aliasing, fallbacks, hosted/user scope), user_id propagation through CustomToolService flows, and tool registry unity_target validation.
Unity Editor Tests & Metadata
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ToolDiscoveryServiceTests.cs, .../ToolDiscoveryServiceTests.cs.meta, TestProjects/UnityMCPTests/Assets/Tests/Editor.meta
Added Editor tests validating EditorPrefs persistence and discovery behavior (including that stored disabled prefs are not overridden) and corresponding Unity meta files.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Middleware as UnityInstanceMiddleware
    participant PluginHub
    participant Registry as SessionRegistry

    Client->>Middleware: LIST /tools (HTTP)
    Middleware->>Registry: resolve_session_id(project_hash, user_id?)
    alt session_id found
        Middleware->>PluginHub: get_tools_for_project(project_hash, user_id)
        PluginHub->>Registry: fetch session by id
        Registry-->>PluginHub: session (tools list)
        PluginHub-->>Middleware: tools list
        Middleware->>Middleware: filter tools by enabled names/aliases/server-only rules
        Middleware-->>Client: filtered tools list
    else no session
        Middleware-->>Client: original/unfiltered tools list
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

codex

Suggested reviewers

  • msanatan
  • dsarno

Poem

🐰 I hopped through prefs and toggles bright,
I nudged transports to reregister right,
Server checks sessions with careful art,
Tests keep toggles true and tools apart,
A tiny rabbit patched the chart 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.35% 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 summarizes the main objective of the PR: fixing issue #709 by preserving tool enabled/disabled state and filtering tools in client listings.
Linked Issues check ✅ Passed The PR successfully addresses all coding requirements from issue #709: persists tool state via EditorPrefs, filters disabled tools from client listings in HTTP/WebSocket modes, and implements reregistration mechanism.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing issue #709. Additional changes like unity_target metadata and user_id propagation support the core objective of tool filtering and management.
Description check ✅ Passed The PR description comprehensively covers all required sections: description, type of change, changes made (organized by component), testing details, documentation status, related issues, and additional notes including limitations.

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

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

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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:

  • In McpToolsSection.ReregisterToolsAsync, consider making the method fully async instead of using ThreadPool.QueueUserWorkItem with .Wait(), so you can avoid blocking a thread-pool thread and more cleanly propagate cancellation or errors.
  • In UnityInstanceMiddleware._resolve_enabled_tool_names_for_context, you sequentially call PluginHub.get_tools_for_project for each project hash; if there can be multiple active sessions, you may want to batch or parallelize these calls (or cache the results) to avoid unnecessary latency on tool listing.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `McpToolsSection.ReregisterToolsAsync`, consider making the method fully async instead of using `ThreadPool.QueueUserWorkItem` with `.Wait()`, so you can avoid blocking a thread-pool thread and more cleanly propagate cancellation or errors.
- In `UnityInstanceMiddleware._resolve_enabled_tool_names_for_context`, you sequentially call `PluginHub.get_tools_for_project` for each project hash; if there can be multiple active sessions, you may want to batch or parallelize these calls (or cache the results) to avoid unnecessary latency on tool listing.

## Individual Comments

### Comment 1
<location> `MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs:248-267` </location>
<code_context>
+            }
+        }
+
+        private void ReregisterToolsAsync()
+        {
+            // Fire and forget - don't block UI
+            ThreadPool.QueueUserWorkItem(_ =>
+            {
+                try
+                {
+                    var transportManager = MCPServiceLocator.TransportManager;
+                    var client = transportManager.GetClient(TransportMode.Http);
+                    if (client != null && client.IsConnected)
+                    {
+                        client.ReregisterToolsAsync().Wait();
+                    }
+                }
+                catch (Exception ex)
+                {
+                    McpLog.Warn($"Failed to reregister tools: {ex.Message}");
</code_context>

<issue_to_address>
**suggestion (performance):** Avoid blocking a ThreadPool thread with .Wait(); use an async flow instead

`ThreadPool.QueueUserWorkItem` plus `client.ReregisterToolsAsync().Wait()` blocks a ThreadPool thread and can cause thread-pool pressure if this is triggered often. Since the goal is just to avoid blocking the UI and `ReregisterToolsAsync` is already async, you can make the method itself fire-and-forget and run the async call directly:

```csharp
private void ReregisterToolsAsync()
{
    var transportManager = MCPServiceLocator.TransportManager;
    var client = transportManager.GetClient(TransportMode.Http);
    if (client == null || !client.IsConnected)
    {
        return;
    }

    _ = Task.Run(async () =>
    {
        try
        {
            await client.ReregisterToolsAsync().ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            McpLog.Warn($"Failed to reregister tools: {ex.Message}");
        }
    });
}
```

This avoids blocking worker threads while still keeping the call off the UI thread and logging failures.

```suggestion
        private void ReregisterToolsAsync()
        {
            // Fire and forget - don't block UI thread
            var transportManager = MCPServiceLocator.TransportManager;
            var client = transportManager.GetClient(TransportMode.Http);
            if (client == null || !client.IsConnected)
            {
                return;
            }

            _ = System.Threading.Tasks.Task.Run(async () =>
            {
                try
                {
                    await client.ReregisterToolsAsync().ConfigureAwait(false);
                }
                catch (Exception ex)
                {
                    McpLog.Warn($"Failed to reregister tools: {ex.Message}");
                }
            });
        }
```
</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: 3

🤖 Fix all issues with AI agents
In `@Server/src/transport/unity_instance_middleware.py`:
- Around line 335-340: The bare except around PluginHub.get_sessions() swallows
all errors; change it to catch Exception as e and log the error at debug level
(matching the file's existing pattern) before returning None so failures are
diagnosable; specifically update the except block that handles the await
PluginHub.get_sessions(user_id=user_id) call to log a message including user_id
and the exception (use the existing logger/process_logger and include exception
info/stacktrace), then keep the return None and preserve assignment to
sessions_data/sessions.
- Around line 362-366: The loop over project_hashes currently swallows all
exceptions from PluginHub.get_tools_for_project(project_hash); update the except
block to catch Exception as e and log the error (including e and the
project_hash) via the existing logger before continuing so failures are visible;
ensure you still continue on error but do not re-raise, and prefer a clear
message like "Failed to fetch tools for project {project_hash}: {e}" to help
debug calls to PluginHub.get_tools_for_project and the handling of
registered_tools.

In
`@TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ToolDiscoveryServiceTests.cs.meta`:
- Line 2: The .meta file contains a hand-crafted GUID
"7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d" which risks collisions; remove or replace
this GUID by letting Unity regenerate a random one—either delete the
corresponding .meta file and reimport the asset or use Unity's Reimport/Refresh
so the GUID is auto-generated, ensuring the GUID value in
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ToolDiscoveryServiceTests.cs.meta
is no longer the manual hex sequence.
🧹 Nitpick comments (6)
MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs (1)

522-527: Unnecessary fully-qualified exception types.

System.OperationCanceledException and System.Exception are fully qualified here, but the System namespace is already imported (line 1). The rest of the file uses unqualified names (e.g., lines 326, 336).

Consistency fix
-            catch (System.OperationCanceledException)
+            catch (OperationCanceledException)
             {
                 McpLog.Warn("[WebSocket] Tool reregistration cancelled");
             }
-            catch (System.Exception ex)
+            catch (Exception ex)
             {
                 McpLog.Error($"[WebSocket] Tool reregistration failed: {ex.Message}");
             }
MCPForUnity/Editor/Services/Transport/IMcpTransportClient.cs (1)

17-17: Consider adding an XML doc comment for the new interface method.

The other members lack docs too, so this is consistent, but since this method has nuanced behavior (no-op for stdio, real reregistration for WebSocket), a brief <summary> would help implementors.

MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs (1)

248-267: Potential for redundant concurrent reregistrations on rapid toggles.

Each individual toggle fires ReregisterToolsAsync on the thread pool independently. Rapid toggling can queue multiple concurrent reregistrations. They'll serialize via the WebSocket send lock, but the redundant work is wasteful.

Consider a simple debounce (e.g., a CancellationTokenSource that gets reset on each call, with a short delay before actually sending) to coalesce rapid individual toggles into a single reregistration. Not a blocker — the current approach is correct, just chatty.

Server/src/transport/unity_instance_middleware.py (1)

58-94: Hardcoded tool name sets are a maintenance burden.

These three collections (_unity_managed_tool_names, _tool_alias_to_unity_target, _server_only_tool_names) must be manually synchronized whenever tools are added, removed, or renamed. If a new Unity-managed tool is introduced without being added here, it will silently remain visible even when disabled (due to the forward-compatibility fallback in _is_tool_visible line 401).

Consider deriving these sets from a shared manifest or registry, or at minimum adding a comment/docstring that cross-references the authoritative source so future maintainers know to update this list.

#!/bin/bash
# Verify whether the hardcoded tool names in _unity_managed_tool_names match
# tool definitions registered elsewhere in the codebase.
echo "=== Tool definitions in C# tool files ==="
rg -n --type=cs 'ToolName\s*=' -g '!*Test*' | head -40

echo ""
echo "=== Python tool registrations (e.g., `@mcp.tool` or name= in tool defs) ==="
rg -n --type=py 'name\s*=\s*"' -g '!*test*' | grep -i tool | head -40

echo ""
echo "=== Aliases like create_script, apply_text_edits in Python ==="
rg -rn 'create_script|apply_text_edits|delete_script|find_in_file|get_sha|script_apply_edits|validate_script' --type=py -g '!*test*' -g '!unity_instance_middleware.py' | head -20
Server/tests/test_transport_characterization.py (1)

271-310: Good coverage of the core filtering path; consider asserting on manage_script too.

The test correctly validates that:

  • Enabled tools pass through (manage_scene)
  • Aliases resolve to their target's enabled state (create_scriptmanage_script)
  • Server-only tools always pass (set_active_instance)
  • Disabled Unity-managed tools are filtered (manage_asset)

manage_script is in the available_tools list and is enabled, so it should appear in the filtered output. Adding an assertion for it would make the test more comprehensive and explicitly document that directly-enabled tools pass through.

Optional: assert manage_script presence
         names = [tool.name for tool in filtered]
+        assert "manage_script" in names
         assert "manage_scene" in names
         assert "create_script" in names
         assert "set_active_instance" in names
         assert "manage_asset" not in names
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ToolDiscoveryServiceTests.cs (1)

116-124: Test assumes at least one built-in tool with AutoRegister=false exists.

Line 124 asserts that such a tool is found. If the codebase ever removes all such tools, this test will fail with a somewhat opaque message. This is acceptable for a characterization test since it documents a current invariant, but worth noting if the tool set changes significantly.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes #709 by persisting Unity tool enabled/disabled preferences across restarts and hiding disabled Unity-managed tools (and aliases) from MCP client tool listings in HTTP/WebSocket mode.

Changes:

  • Unity (C#): Persist tool enabled state and trigger tool re-registration when toggles change.
  • Server (Python): Filter list_tools results based on the enabled tool set registered for the active Unity session (including alias mapping).
  • Tests: Add characterization tests for transport tool listing behavior and Unity preference persistence tests.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
TestProjects/UnityMCPTests/Assets/Tests/Editor/State.meta Adds Unity folder metadata for new test structure.
TestProjects/UnityMCPTests/Assets/Tests/Editor.meta Adds Unity folder metadata for Editor tests folder.
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ToolDiscoveryServiceTests.cs.meta Adds Unity meta for new C# test file.
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ToolDiscoveryServiceTests.cs Adds EditMode tests validating EditorPrefs persistence and discovery behavior.
Server/tests/test_transport_characterization.py Adds characterization tests for HTTP tool-list filtering and alias behavior.
Server/src/transport/unity_instance_middleware.py Implements on_list_tools() filtering logic based on Unity session enabled tools.
MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs Triggers tool re-registration after toggle changes / bulk enable-disable.
MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs Adds ReregisterToolsAsync() to re-send tool registration while connected.
MCPForUnity/Editor/Services/Transport/Transports/StdioTransportClient.cs Adds no-op ReregisterToolsAsync() for stdio transport.
MCPForUnity/Editor/Services/Transport/TransportManager.cs Exposes GetClient() publicly for UI-triggered reregistration.
MCPForUnity/Editor/Services/Transport/IMcpTransportClient.cs Extends transport interface with ReregisterToolsAsync().
MCPForUnity/Editor/Services/ToolDiscoveryService.cs Adjusts preference initialization logic to avoid overriding stored disabled state.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 10754e5819

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 11, 2026

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":401,"request":{"method":"PATCH","url":"https://api.github.com/repos/CoplayDev/unity-mcp/issues/comments/3885153002","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.6 Node.js/24","authorization":"token [REDACTED]","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- walkthrough_start -->\n\n<details>\n<summary>📝 Walkthrough</summary>\n\n## Walkthrough\nAdds tool re-registration support across transports and UI, adjusts ToolDiscoveryService initialization, introduces server-side Unity-aware tool filtering for tool listing, and expands tests. TransportManager exposes client retrieval publicly; WebSocket client implements re-registration; stdio is a no-op. Editor UI batches re-registration on bulk changes. Server middleware filters tools by enabled set from Unity sessions.\n\n## Changes\n|Cohort / File(s)|Summary|\n|---|---|\n|**Tool Discovery and Tests** <br> `MCPForUnity/Editor/Services/ToolDiscoveryService.cs`, `TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ToolDiscoveryServiceTests.cs`, `TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ToolDiscoveryServiceTests.cs.meta`, `TestProjects/UnityMCPTests/Assets/Tests/Editor.meta`, `TestProjects/UnityMCPTests/Assets/Tests/Editor/State.meta`|Simplified preference initialization to one-time default without overrides; added comprehensive EditorPrefs-based tests and Unity meta files.|\n|**Transport Re-registration API** <br> `MCPForUnity/Editor/Services/Transport/IMcpTransportClient.cs`, `MCPForUnity/Editor/Services/Transport/TransportManager.cs`, `MCPForUnity/Editor/Services/Transport/Transports/StdioTransportClient.cs`, `MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs`|Introduced `ReregisterToolsAsync` to interface; made `TransportManager.GetClient` public; implemented no-op for stdio and active re-registration with logging for WebSocket.|\n|**Editor UI Re-registration Flow** <br> `MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs`|Added async re-registration trigger; batched re-registration after bulk toggles; updated toggle handling parameters and threading usage.|\n|**Server Middleware Filtering** <br> `Server/src/transport/unity_instance_middleware.py`|Added Unity-aware filtering in `on_list_tools` for HTTP transport using PluginHub/session context; resolves enabled tools per project/session and filters tool list accordingly.|\n|**Server Tests** <br> `Server/tests/test_transport_characterization.py`|Added tests validating tool list filtering behavior and fallback when no enabled tool set is available.|\n\n## Sequence Diagram(s)\n```mermaid\nsequenceDiagram\n  autonumber\n  actor User as User\n  participant UI as UI (McpToolsSection)\n  participant TM as TransportManager\n  participant WS as WebSocketTransportClient\n  participant STD as StdioTransportClient\n  participant Srv as Server\n\n  User->>UI: Toggle tool(s)\n  UI->>UI: Update state / batch changes\n  UI->>TM: GetClient(mode)\n  alt WebSocket mode\n    UI->>WS: ReregisterToolsAsync()\n    WS->>WS: Check IsConnected / token\n    WS->>Srv: SendRegisterToolsAsync (re-register)\n    Srv-->>WS: Ack/Result\n    WS-->>UI: Complete\n  else Stdio mode\n    UI->>STD: ReregisterToolsAsync()\n    STD-->>UI: No-op complete\n  end\n```\n\n```mermaid\nsequenceDiagram\n  autonumber\n  actor Client as MCP Client\n  participant Srv as Server\n  participant MW as UnityInstanceMiddleware\n  participant Hub as PluginHub\n  participant U as Unity Session(s)\n\n  Client->>Srv: list_tools\n  Srv->>MW: on_list_tools(context)\n  MW->>MW: Detect HTTP + Unity context\n  alt Enabled set available\n    MW->>Hub: Get enabled tools for project(s)\n    Hub->>U: Query active session(s)\n    U-->>Hub: Enabled tool names/aliases\n    Hub-->>MW: Aggregated enabled set\n    MW->>Srv: Filtered tool list\n  else No enabled set\n    MW->>Srv: Unfiltered tool list\n  end\n  Srv-->>Client: Tool list response\n```\n\n## Estimated code review effort\n🎯 4 (Complex) | ⏱️ ~60 minutes\n\n## Possibly related PRs\n- CoplayDev/unity-mcp#418: Changes tool discovery/enablement and transport registration flows, overlapping with this PR’s discovery logic and re-registration APIs.\n- CoplayDev/unity-mcp#656: Modifies unity_instance_middleware PluginHub/session handling, closely related to the new Unity-aware tool filtering.\n- CoplayDev/unity-mcp#220: Adds/edit-mode unit tests within UnityMCPTests project, related to the new test suite added here.\n\n## Suggested labels\n`codex`\n\n## Suggested reviewers\n- dsarno\n- msanatan\n\n## Poem\n> A rabbit taps the toggle light—click!\n> Tools hop in line, no extra tricks.\n> WebSockets whisper, “register anew,”\n> While stdio naps, with nothing to do.\n> Server sifts the burrow’s list,\n> Only the chosen make the tryst.\n> Thump-thump—settings now persist! 🐇✨\n\n</details>\n\n<!-- walkthrough_end -->\n\n\n<!-- pre_merge_checks_walkthrough_start -->\n\n<details>\n<summary>🚥 Pre-merge checks | ✅ 4 | ❌ 1</summary>\n\n<details>\n<summary>❌ Failed checks (1 warning)</summary>\n\n|     Check name     | Status     | Explanation                                                                           | Resolution                                                                         |\n| :----------------: | :--------- | :------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------- |\n| Docstring Coverage | ⚠️ Warning | Docstring coverage is 17.50% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. |\n\n</details>\n<details>\n<summary>✅ Passed checks (4 passed)</summary>\n\n|         Check name         | Status   | Explanation                                                                                                                                                                                                                                                                                                                                                                                      |\n| :------------------------: | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n|         Title check        | ✅ Passed | The title clearly and specifically describes the main change: preserving tool enabled/disabled state and filtering tools in client listings, with reference to issue `#709`.                                                                                                                                                                                                                       |\n|     Linked Issues check    | ✅ Passed | The code changes fully address issue `#709` objectives: tools disabled in Unity remain disabled after restart via EditorPrefs persistence [ToolDiscoveryService], reregistration is triggered when toggles change [McpToolsSection, IMcpTransportClient, WebSocketTransportClient], and disabled tools are filtered from client listings via server-side filtering [unity_instance_middleware.py]. |\n| Out of Scope Changes check | ✅ Passed | All code changes are within scope of issue `#709`: persistence of tool state, triggering re-registration, and server-side filtering. Public API additions (GetClient, ReregisterToolsAsync) support the core feature and are minimally invasive. Test additions and documentation updates support the feature appropriately.                                                                       |\n|      Description Check     | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled.                                                                                                                                                                                                                                                                                                                                      |\n\n</details>\n\n<sub>✏️ Tip: You can configure your own custom pre-merge checks in the settings.</sub>\n\n</details>\n\n<!-- pre_merge_checks_walkthrough_end -->\n\n<!-- finishing_touch_checkbox_start -->\n\n<details>\n<summary>✨ Finishing touches</summary>\n\n- [ ] <!-- {\"checkboxId\": \"7962f53c-55bc-4827-bfbf-6a18da830691\"} --> 📝 Generate docstrings\n<details>\n<summary>🧪 Generate unit tests (beta)</summary>\n\n- [ ] <!-- {\"checkboxId\": \"f47ac10b-58cc-4372-a567-0e02b2c3d479\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Create PR with unit tests\n- [ ] <!-- {\"checkboxId\": \"07f1e7d6-8a8e-4e23-9900-8731c2c87f58\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Post copyable unit tests in a comment\n- [ ] <!-- {\"checkboxId\": \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Commit unit tests in branch `fix/preserve-tool-toggle-state-restarts`\n\n</details>\n\n</details>\n\n<!-- finishing_touch_checkbox_end -->\n\n<!-- tips_start -->\n\n---\n\nThanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=CoplayDev/unity-mcp&utm_content=723)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.\n\n<details>\n<summary>❤️ Share</summary>\n\n- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)\n- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)\n- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)\n- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)\n\n</details>\n\n<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>\n\n<!-- tips_end -->\n\n<!-- internal state start -->\n\n\n<!-- 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 -->\n\n<!-- internal state end -->"},"request":{"retryCount":1}},"response":{"url":"https://api.github.com/repos/CoplayDev/unity-mcp/issues/comments/3885153002","status":401,"headers":{"access-control-allow-origin":"*","access-control-expose-headers":"ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset","connection":"close","content-security-policy":"default-src 'none'","content-type":"application/json; charset=utf-8","date":"Wed, 11 Feb 2026 16:02:46 GMT","referrer-policy":"origin-when-cross-origin, strict-origin-when-cross-origin","server":"github.com","strict-transport-security":"max-age=31536000; includeSubdomains; preload","vary":"Accept-Encoding, Accept, X-Requested-With","x-content-type-options":"nosniff","x-frame-options":"deny","x-github-media-type":"github.v3; format=json","x-github-request-id":"907C:22B9F2:C7A95A:3628CE7:698CA826","x-xss-protection":"0"},"data":{"message":"Bad credentials","documentation_url":"https://docs.github.com/rest","status":"401"}}}

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.

Caution

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

⚠️ Outside diff range comments (1)
Server/src/transport/plugin_hub.py (1)

340-354: ⚠️ Potential issue | 🟠 Major

get_tool_definition lacks user_id parameter, inconsistent with updated get_tools_for_project.

In remote-hosted mode, get_tools_for_project (line 330) accepts user_id and passes it to get_session_id_by_hash, but get_tool_definition (line 341) does not, causing user-scoped session lookups to fail. The proposed fix to add user_id to get_tool_definition is correct:

Proposed fix
     `@classmethod`
-    async def get_tool_definition(cls, project_hash: str, tool_name: str) -> Any | None:
+    async def get_tool_definition(cls, project_hash: str, tool_name: str, user_id: str | None = None) -> Any | None:
         """Retrieve a specific tool definition for an active project hash."""
         if cls._registry is None:
             return None
 
-        session_id = await cls._registry.get_session_id_by_hash(project_hash)
+        session_id = await cls._registry.get_session_id_by_hash(project_hash, user_id=user_id)
         if not session_id:
             return None

Note: This fix alone is incomplete—the service layer (CustomToolService.list_registered_tools and get_tool_definition) also needs to accept and thread through user_id from the MCP context to reach the PluginHub methods.

🧹 Nitpick comments (1)
Server/src/transport/unity_instance_middleware.py (1)

58-94: Categorization metadata could be derived from tool decorators.

The three hardcoded sets (_unity_managed_tool_names, _tool_alias_to_unity_target, _server_only_tool_names) categorize tools for filtering, not duplicate registration—tools are auto-discovered via @mcp_for_unity_tool decorators. The filtering logic is sound: unknown tools intentionally remain visible for forward compatibility rather than being hidden if omitted from these sets.

That said, you could reduce the explicit hardcoding by adding a category parameter to the decorator (e.g., @mcp_for_unity_tool(category="unity_managed")), then deriving these sets from tool metadata at runtime. This would keep the categorization and tool definition in sync automatically, though it adds minimal ongoing maintenance compared to the current explicit sets given their size and stability.

@whatevertogo whatevertogo force-pushed the fix/preserve-tool-toggle-state-restarts branch from b7759be to ceb2396 Compare February 11, 2026 16:29
whatevertogo and others added 4 commits February 12, 2026 00:44
- Pass user_id to execute_tool for user-scoped tool lookups
- Update get_tool_definition to support user-scoped queries

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…oplayDev#709)

Comprehensive fix for Issue CoplayDev#709:
- Unity tools stay disabled across restarts (EditorPrefs persistence)
- Client tool lists filter out disabled Unity tools (HTTP mode only)
- Script helper tools follow their target tool's enabled state
- Added graceful error fallback when tool lookup fails

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@whatevertogo whatevertogo force-pushed the fix/preserve-tool-toggle-state-restarts branch from ceb2396 to fc81d9e Compare February 11, 2026 16:46
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.

Caution

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

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

354-370: ⚠️ Potential issue | 🟡 Minor

Pre-existing bug: async function used as thread target will never execute.

Not introduced by this PR, but _flip_async is an async def passed to threading.Thread(target=...). This creates a coroutine that is never awaited — the thread completes immediately without running the body. Wrapping with asyncio.run(_flip_async()) or using asyncio.ensure_future on an existing loop would be needed for the code to actually execute.

🧹 Nitpick comments (3)
MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs (1)

248-249: Async suffix on a void-returning method is misleading.

By .NET convention, the Async suffix signals a method returns Task/Task<T>. Since this is intentionally fire-and-forget, consider renaming to ReregisterToolsFireAndForget or TriggerToolReregistration to avoid confusion with awaitable methods.

Server/src/services/registry/tool_registry.py (1)

37-41: Minor: prefer pop over if key in dict + del.

Ruff RUF051 flags this pattern. Using pop is more concise.

Proposed simplification
-        tool_kwargs = dict(kwargs)  # Create a copy to avoid side effects
-        if "unity_target" in tool_kwargs:
-            del tool_kwargs["unity_target"]
+        tool_kwargs = dict(kwargs)  # Create a copy to avoid side effects
+        tool_kwargs.pop("unity_target", None)
Server/tests/test_transport_characterization.py (1)

284-648: Consider extracting the shared test setup into a helper or fixture to reduce deep nesting.

Every on_list_tools filtering test repeats the same ~15-line with patch(...) nesting structure. A helper method or pytest fixture yielding the mocks would significantly reduce boilerplate and improve readability.

Example sketch
from contextlib import asynccontextmanager

`@asynccontextmanager`
async def _patch_for_list_tools_filter(middleware, mock_context, *, sessions, tools_side_effect, registry=None):
    middleware_ctx = Mock()
    middleware_ctx.fastmcp_context = mock_context

    with patch.object(middleware, "_inject_unity_instance", new=AsyncMock()):
        with patch("transport.unity_instance_middleware.PluginHub.is_configured", return_value=True):
            with patch("transport.unity_instance_middleware.get_registered_tools",
                       return_value=registry or _tool_registry_for_visibility_tests()):
                with patch("transport.unity_instance_middleware.PluginHub.get_sessions",
                           new_callable=AsyncMock) as mock_sessions:
                    with patch("transport.unity_instance_middleware.PluginHub.get_tools_for_project",
                               new_callable=AsyncMock) as mock_tools:
                        mock_sessions.return_value = sessions
                        if callable(tools_side_effect):
                            mock_tools.side_effect = tools_side_effect
                        else:
                            mock_tools.return_value = tools_side_effect
                        yield middleware_ctx, mock_sessions, mock_tools

@whatevertogo whatevertogo marked this pull request as draft February 11, 2026 19:07
@whatevertogo whatevertogo marked this pull request as ready for review February 11, 2026 19:34
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 reviewed your changes and they look great!


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

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1a9b08351f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@whatevertogo whatevertogo marked this pull request as draft February 12, 2026 01:29
@whatevertogo whatevertogo marked this pull request as ready for review February 12, 2026 02:00
@whatevertogo
Copy link
Contributor Author

1064 line for test, 40 for .meta,: add 200 lines code

IntegerField lives in UnityEditor.UIElements, not UnityEngine.UIElements.
Without this import the project fails to compile.
@dsarno dsarno closed this pull request by merging all changes into CoplayDev:beta in 73f2295 Feb 12, 2026
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: Disabled Tools Reappear and Still Show Up in Client Lists

2 participants