Skip to content

fix: preserve tool toggle state and filter tools in client listings (rebased #723)#735

Merged
dsarno merged 11 commits intoCoplayDev:betafrom
dsarno:fix/723-rebased
Feb 12, 2026
Merged

fix: preserve tool toggle state and filter tools in client listings (rebased #723)#735
dsarno merged 11 commits intoCoplayDev:betafrom
dsarno:fix/723-rebased

Conversation

@dsarno
Copy link
Collaborator

@dsarno dsarno commented Feb 12, 2026

Rebased version of #723 by @whatevertogo to resolve merge conflicts after #688 landed on beta.

Original PR: #723 | Original author: @whatevertogo

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

Conflict resolution: kept both ForceStop() from #688 and public GetClient() from #723 in TransportManager.cs.

Bug fix included: added missing using UnityEditor.UIElements for IntegerField (pushed directly to author branch).

Summary by Sourcery

Align Unity tool enablement between client and server, ensuring disabled tools persist across sessions and are correctly filtered from tool listings in HTTP/hosted modes.

Bug Fixes:

  • Ensure Unity Editor tool enabled/disabled state is persisted via EditorPrefs and respected across ToolDiscoveryService instances.
  • Fix missing UnityEditor.UIElements import to support IntegerField usage in the tools UI.

Enhancements:

  • Add user-scoped tool registration and lookup so remote-hosted users with shared project hashes see only their own registered custom tools.
  • Introduce registry-driven tool visibility metadata and middleware filtering so Unity-managed tools and their aliases are hidden when disabled while keeping server-only tools visible.
  • Add support for declaring server-only and alias Unity tools via mcp_for_unity_tool metadata without leaking internal flags into MCP annotations.
  • Expose a client-facing transport method to trigger tool re-registration over WebSocket when tool toggles change, while leaving stdio behavior unchanged.

Tests:

  • Add extensive middleware tests covering tool filtering behavior, alias handling, failure and stale-state fallbacks, and multi-session union of enabled tools.
  • Add tests verifying user-scoped behavior of PluginHub and CustomToolService for tool listing, lookup, and execution APIs.
  • Add tests for tool registry metadata behavior, including unity_target defaults, server-only and alias handling, and validation of invalid values.
  • Add Unity edit-mode tests for ToolDiscoveryService to ensure EditorPrefs-backed enablement and persistence semantics.

Summary by CodeRabbit

  • New Features

    • Added dynamic tool reregistration capability, allowing tools to be updated after enabling/disabling without restart.
    • Implemented user-scoped tool visibility in multi-user hosting scenarios.
    • Added HTTP-based tool filtering for better project-aware tool management.
  • Bug Fixes

    • Simplified tool preference initialization logic.

whatevertogo and others added 11 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>
- 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>
IntegerField lives in UnityEditor.UIElements, not UnityEngine.UIElements.
Without this import the project fails to compile.
…hub.com/whatevertogo/unity-mcp into fix/723-rebased

# Conflicts:
#	MCPForUnity/Editor/Services/Transport/TransportManager.cs
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Feb 12, 2026

Reviewer's Guide

Implements persistent Unity tool enable/disable state with server-side visibility filtering and user-scoped tool registration, plus client support for re-registering tools over HTTP/WebSocket, while tightening tool registry metadata and adding comprehensive tests.

Sequence diagram for Unity-aware MCP tool listing with visibility filtering

sequenceDiagram
    actor User
    participant UnityEditor as UnityEditor
    participant HttpClient as HttpMcpClient
    participant Server as FastMCP_Server
    participant Middleware as UnityInstanceMiddleware
    participant PluginHub as PluginHub
    participant Registry as ToolRegistry

    User->>UnityEditor: Open tools panel / request tools
    UnityEditor->>HttpClient: list_tools()
    HttpClient->>Server: HTTP list_tools request
    Server->>Middleware: on_list_tools(context, call_next)
    activate Middleware
    Middleware->>Middleware: _inject_unity_instance(context)
    Middleware->>Server: call_next(context)
    Server-->>Middleware: tools (all_tools)

    Middleware->>Middleware: _should_filter_tool_listing()
    alt transport is http and PluginHub.is_configured()
        Middleware->>Middleware: _refresh_tool_visibility_metadata_from_registry()
        activate Middleware
        Middleware->>Registry: get_registered_tools()
        Registry-->>Middleware: registry_tools
        Middleware->>Middleware: update _unity_managed_tool_names
        Middleware->>Middleware: update _tool_alias_to_unity_target
        Middleware->>Middleware: update _server_only_tool_names
        deactivate Middleware

        Middleware->>Middleware: _resolve_enabled_tool_names_for_context(context)
        activate Middleware
        Middleware->>PluginHub: get_sessions(user_id)
        PluginHub-->>Middleware: SessionList
        Middleware->>Middleware: _resolve_candidate_project_hashes(active_instance)
        loop for each project_hash
            Middleware->>PluginHub: get_tools_for_project(project_hash, user_id)
            PluginHub->>Registry: get_session_id_by_hash(project_hash, user_id)
            Registry-->>PluginHub: session_id
            PluginHub-->>Middleware: tools_for_project
            Middleware->>Middleware: accumulate enabled_tool_names
        end
        Middleware-->>Middleware: enabled_tool_names
        deactivate Middleware

        loop for tool in tools
            Middleware->>Middleware: _is_tool_visible(tool.name, enabled_tool_names)
            alt visible
                Middleware->>Middleware: add tool to filtered list
            end
        end
        Middleware-->>Server: filtered tools
    else no filtering
        Middleware-->>Server: tools (unmodified)
    end
    deactivate Middleware

    Server-->>HttpClient: tool list (filtered)
    HttpClient-->>UnityEditor: tool list (filtered)
    UnityEditor-->>User: Render enabled tools
Loading

Sequence diagram for Unity tool toggle and server re-registration

sequenceDiagram
    actor User
    participant ToolsUI as McpToolsSection
    participant ToolDiscovery as ToolDiscoveryService
    participant TransportMgr as TransportManager
    participant HttpClient as HttpTransportClient
    participant WsClient as WebSocketTransportClient
    participant Server as FastMCP_Server

    User->>ToolsUI: Toggle tool checkbox
    ToolsUI->>ToolsUI: HandleToggleChange(tool, enabled, updateSummary, reregisterTools)
    ToolsUI->>ToolDiscovery: SetToolEnabled(tool.Name, enabled)
    ToolDiscovery-->>ToolsUI: preference updated
    alt updateSummary
        ToolsUI->>ToolsUI: UpdateSummary()
    end
    alt reregisterTools
        ToolsUI->>ToolsUI: ReregisterToolsAsync()
        ToolsUI->>TransportMgr: GetClient(TransportMode.Http)
        TransportMgr-->>ToolsUI: HttpClient or null
        alt client is null or !IsConnected
            ToolsUI-->>User: Local state updated, no server re-register
        else client connected
            ToolsUI->>HttpClient: ReregisterToolsAsync()
            activate HttpClient
            HttpClient->>WsClient: (HTTP path) or internal transport to server
            WsClient->>WsClient: SendRegisterToolsAsync(token)
            WsClient->>Server: register_tools payload
            Server-->>WsClient: register_tools response
            WsClient-->>HttpClient: reregistration complete
            deactivate HttpClient
            HttpClient-->>ToolsUI: ReregisterToolsAsync completed
        end
    end

    User->>ToolsUI: Click Enable All / Disable All
    ToolsUI->>ToolsUI: SetAllToolsState(enabled)
    ToolsUI->>ToolDiscovery: IsToolEnabled(tool.Name) for each tool
    loop tools
        alt state differs
            ToolsUI->>ToolDiscovery: SetToolEnabled(tool.Name, enabled)
            ToolsUI->>ToolsUI: HandleToggleChange(tool, enabled, updateSummary=false, reregisterTools=false)
        end
    end
    ToolsUI->>ToolsUI: UpdateSummary()
    alt any changes
        ToolsUI->>ToolsUI: ReregisterToolsAsync() (single bulk call)
    end
Loading

Class diagram for transport clients and tool re-registration

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

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

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

    class TransportManager {
        -IMcpTransportClient _httpClient
        -IMcpTransportClient _stdioClient
        +Task~bool~ StartAsync(TransportMode mode)
        +Task StopAsync(TransportMode mode)
        +Task~bool~ VerifyAsync(TransportMode mode)
        +void ForceStop(TransportMode mode)
        +IMcpTransportClient GetClient(TransportMode mode)
        -IMcpTransportClient GetOrCreateClient(TransportMode mode)
        -void UpdateState(TransportMode mode, TransportState state)
    }

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

    IMcpTransportClient <|.. WebSocketTransportClient
    IMcpTransportClient <|.. StdioTransportClient

    TransportManager --> IMcpTransportClient : uses
    McpToolsSection --> TransportManager : uses
    McpToolsSection ..> IMcpTransportClient : calls ReregisterToolsAsync()

    class ToolMetadata {
        +string Name
        +bool AutoRegister
        +bool IsBuiltIn
    }

    class Toggle {
        +void SetValueWithoutNotify(bool value)
    }

    McpToolsSection --> ToolMetadata
    McpToolsSection --> Toggle
Loading

Class diagram for tool registry decorator and visibility metadata

classDiagram
    class ToolRegistryModule {
        +list~dict~ _tool_registry
        +Callable mcp_for_unity_tool(str name, str description, str unity_target, **kwargs)
    }

    class mcp_for_unity_tool_decorator {
        -str tool_name
        -str_or_none normalized_unity_target
        -dict tool_kwargs
        +Callable __call__(Callable func)
    }

    ToolRegistryModule o--> mcp_for_unity_tool_decorator

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

    class PluginHub {
        +async classmethod get_sessions(str user_id)
        +async classmethod get_tools_for_project(str project_hash, str user_id)
        +async classmethod get_tool_definition(str project_hash, str tool_name, str user_id)
    }

    class ToolInfoDict {
        +str name
        +str_or_none unity_target
        +dict kwargs
    }

    UnityInstanceMiddleware --> ToolRegistryModule : calls get_registered_tools()
    UnityInstanceMiddleware --> PluginHub : queries sessions and tools
    ToolRegistryModule --> ToolInfoDict : stores metadata
Loading

Class diagram for CustomToolService and user-scoped tool execution

classDiagram
    class CustomToolService {
        -dict~str, dict~str, ToolDefinitionModel~~ _project_tools
        +static CustomToolService get_instance()
        +async JSONResponse register_tools(Request request)
        +async list_registered_tools(str project_id, str user_id)
        +async get_tool_definition(str project_id, str tool_name, str user_id)
        +async execute_tool(str project_id, str tool_name, str unity_instance, dict params, str user_id) MCPResponse
        -async MCPResponse _poll_until_complete(str project_id, str tool_name, str unity_instance, dict initial_params, MCPResponse initial_response, str poll_action, str user_id)
    }

    class ToolDefinitionModel {
        +str name
        +bool requires_polling
        +str_or_none poll_action
        +str description
    }

    class Context {
        +object get_state(str key)
    }

    class CustomToolsResource {
        +async CustomToolsResourceResponse get_custom_tools(Context ctx)
    }

    class ExecuteCustomToolHandler {
        +async MCPResponse execute_custom_tool(Context ctx, str tool_name, dict parameters)
    }

    class HelperFunctions {
        +str_or_none get_user_id_from_context(Context ctx)
    }

    class PluginHub {
        +async classmethod get_tools_for_project(str project_hash, str user_id)
        +async classmethod get_tool_definition(str project_hash, str tool_name, str user_id)
    }

    CustomToolService --> ToolDefinitionModel
    CustomToolsResource --> CustomToolService : list_registered_tools(project_id, user_id)
    ExecuteCustomToolHandler --> CustomToolService : execute_tool(project_id, tool_name, unity_instance, parameters, user_id)
    CustomToolService --> PluginHub : get_tools_for_project, get_tool_definition
    HelperFunctions --> Context
    CustomToolsResource --> HelperFunctions : get_user_id_from_context(ctx)
    ExecuteCustomToolHandler --> HelperFunctions : get_user_id_from_context(ctx)
Loading

File-Level Changes

Change Details Files
Add UnityInstanceMiddleware-based filtering of list_tools responses using registry metadata and PluginHub session/tool state, including alias handling and multiple-session union semantics.
  • Introduce on_list_tools in UnityInstanceMiddleware to filter tools based on enabled Unity tools resolved from PluginHub sessions and tools-for-project lookups, with graceful fallback when lookups fail or state is stale.
  • Cache and periodically refresh tool visibility metadata from the server-side tool registry (unity-managed tools, aliases, server-only tools) using a signature and locks to avoid unnecessary recomputation.
  • Implement alias/target semantics and server-only behavior so that aliases follow their Unity target tool state and unknown tools remain visible for forward compatibility.
Server/src/transport/unity_instance_middleware.py
Server/tests/test_transport_characterization.py
Server/tests/test_tool_registry_metadata.py
Extend tool registry and server custom tool services to support unity_target metadata and user-scoped tool registration/lookup in hosted mode.
  • Augment mcp_for_unity_tool decorator to capture a normalized unity_target (self, alias, or None) without leaking it into mcp.tool kwargs, and expose it via get_registered_tools.
  • Thread optional user_id through CustomToolService list_registered_tools, get_tool_definition, execute_tool and polling helpers, and into PluginHub get_tools_for_project/get_tool_definition and send_with_unity_instance invocations.
  • Ensure PluginHub get_tools_for_project/get_tool_definition query sessions by (user_id, project_hash) in remote-hosted mode and add tests to verify user scoping for shared project hashes.
Server/src/services/registry/tool_registry.py
Server/src/services/custom_tool_service.py
Server/src/services/resources/custom_tools.py
Server/src/services/tools/execute_custom_tool.py
Server/src/transport/plugin_hub.py
Server/tests/test_custom_tool_service_user_scope.py
Server/tests/test_transport_characterization.py
Server/tests/test_tool_registry_metadata.py
Update Unity editor client to persist tool toggle preferences and trigger tool re-registration to the HTTP/WebSocket server when tool visibility changes.
  • Adjust McpToolsSection toggle handling to optionally suppress re-registration for bulk changes, then fire a single asynchronous ReregisterToolsAsync call via TransportManager.GetClient when needed.
  • Expose IMcpTransportClient.ReregisterToolsAsync and implement it for WebSocketTransportClient (sending register_tools again) and as a no-op for StdioTransportClient.
  • Simplify ToolDiscoveryService preference initialization by removing logic that could override a stored false value for built-in tools, and add edit-mode tests to confirm EditorPrefs-based persistence semantics across service instances and discovery calls.
  • Restore and reintroduce a public TransportManager.GetClient while keeping ForceStop from a conflicting PR.
MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs
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/Services/ToolDiscoveryService.cs
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ToolDiscoveryServiceTests.cs

Assessment against linked issues

Issue Objective Addressed Explanation
#709 Ensure disabled MCP tools remain disabled across Unity Editor restarts by correctly persisting and honoring their enabled/disabled state.
#709 Ensure tools that are disabled in Unity do not appear in MCP client tool lists (e.g., Claude Code / Trae) by filtering server tool listings based on the enabled tool set.

Possibly linked issues

  • #N/A: PR implements persistence of tool toggle state and server-side filtering so disabled tools disappear from client lists.

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

@dsarno dsarno merged commit 73f2295 into CoplayDev:beta Feb 12, 2026
1 check passed
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 12, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

This PR implements dynamic tool reregistration and user-scoped visibility filtering to fix disabled tools reappearing and remaining visible to clients. The system removes forced-enable logic, adds transport-layer reregistration when tools are toggled, introduces registry-based metadata for tool targeting, propagates user identities through service methods, and adds server-side filtering of tools based on enabled state and user context.

Changes

Cohort / File(s) Summary
Tool Discovery & Persistence
MCPForUnity/Editor/Services/ToolDiscoveryService.cs
Removed logic that forced built-in non-auto-registered tools to enabled state after initialization, allowing disabled preferences to persist.
Transport Interface & Implementation
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
Exposed public GetClient(TransportMode) method in TransportManager; added ReregisterToolsAsync() to interface and implementations (no-op for stdio, active for WebSocket with connection checks and error handling).
UI Tool Management
MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs
Extended HandleToggleChange with reregistration parameter; refactored SetAllToolsState to batch updates and trigger single reregistration after bulk operations; added async fire-and-forget reregistration calls gated by connectivity.
Server Tool Service & User Scoping
Server/src/services/custom_tool_service.py, Server/src/services/resources/custom_tools.py
Added get_user_id_from_context() helper; propagated optional user_id parameter through list_registered_tools, get_tool_definition, execute_tool, and _poll_until_complete; integrated user extraction in global tool handler.
Tool Registry Metadata
Server/src/services/registry/tool_registry.py, Server/src/services/tools/debug_request_context.py, Server/src/services/tools/execute_custom_tool.py, Server/src/services/tools/find_in_file.py, Server/src/services/tools/manage_script.py, Server/src/services/tools/script_apply_edits.py, Server/src/services/tools/set_active_instance.py
Added unity_target parameter to mcp_for_unity_tool decorator with validation and normalization; updated tool registrations to include target metadata while preventing parameter leakage into kwargs.
Plugin Hub & Tool Resolution
Server/src/transport/plugin_hub.py
Added optional user_id parameter to get_tools_for_project() and get_tool_definition() methods; propagated user scope to registry lookups for multi-tenant resolution.
Server-Side Tool Visibility Filtering
Server/src/transport/unity_instance_middleware.py
Introduced on_list_tools middleware hook with registry-driven visibility model; added tool filtering by server-only status, enabled set, and unity-target aliases; implemented session/project resolution and throttled metadata refresh from registry.
Editor Tests
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ToolDiscoveryServiceTests.cs, TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ToolDiscoveryServiceTests.cs.meta, TestProjects/UnityMCPTests/Assets/Tests/Editor.meta
Added NUnit test suite validating tool preference persistence, EditorPrefs integration, and tool state across service instances.
Server Tests
Server/tests/test_custom_tool_service_user_scope.py, Server/tests/test_tool_registry_metadata.py, Server/tests/test_transport_characterization.py
Added comprehensive test coverage for user-scoped tool execution, unity-target metadata normalization/validation, and server-side tool visibility filtering under HTTP transport mode.

Sequence Diagram(s)

sequenceDiagram
    participant User as User (UI)
    participant UI as McpToolsSection
    participant Transport as WebSocket Transport
    participant Server as MCP Server

    User->>UI: Toggle tool enabled/disabled
    UI->>UI: Update tool state in batch
    UI->>UI: Detect changes occurred
    UI->>Transport: ReregisterToolsAsync()
    Transport->>Transport: Check if connected
    alt Connected
        Transport->>Server: SendRegisterToolsAsync()
        Server->>Server: Update registered tools
        Server-->>Transport: Success
        Transport-->>UI: Task completed
    else Not Connected
        Transport-->>UI: Warning logged, Task completed
    end
Loading
sequenceDiagram
    participant Client as MCP Client
    participant Middleware as UnityInstanceMiddleware
    participant PluginHub as PluginHub
    participant Registry as Tool Registry

    Client->>Middleware: Request list_tools
    Middleware->>Middleware: Inject Unity instance data
    alt Should filter (HTTP mode)
        Middleware->>Registry: Refresh visibility metadata
        Registry-->>Middleware: Tool visibility sets/mappings
        Middleware->>PluginHub: Get sessions and projects
        PluginHub-->>Middleware: Session/project data
        Middleware->>PluginHub: Get tools_for_project(user_id)
        PluginHub-->>Middleware: User-scoped tools
        Middleware->>Middleware: Filter by visibility and enabled state
    end
    Middleware-->>Client: Filtered tool list
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 The tools were lost, forgotten, gone,
But now they dance from dusk to dawn!
With targets set and users known,
Each tool returns to find its home.
Hop-hop, the registry now leads the way! ✨

✨ 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.

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 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `Server/src/transport/unity_instance_middleware.py:396-400` </location>
<code_context>
+                self._last_tool_visibility_refresh = now
+                return
+
+            if not registry_tools and not self._has_logged_empty_registry_warning:
+                logger.warning(
+                    "Tool registry is empty during tool-list filtering; treating tools as unknown/visible."
+                )
+                self._has_logged_empty_registry_warning = True
+            elif registry_tools:
+                self._has_logged_empty_registry_warning = False
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Empty registry warning message does not match actual behavior when previous metadata exists

When `registry_tools` is empty you log that tools are treated as unknown/visible, but the existing `_unity_managed_tool_names`, `_tool_alias_to_unity_target`, and `_server_only_tool_names` remain unchanged. If the registry was previously populated, tools still follow the old metadata. Please either update the log message to state that existing metadata is retained, or clear these metadata structures when the registry is empty so behavior matches the message.
</issue_to_address>

### Comment 2
<location> `Server/src/services/custom_tool_service.py:32-41` </location>
<code_context>
 _MAX_POLL_SECONDS = 600


+def get_user_id_from_context(ctx: Context) -> str | None:
+    """Read user_id from request-scoped context in remote-hosted mode."""
+    if not config.http_remote_hosted:
+        return None
+
+    get_state = getattr(ctx, "get_state", None)
+    if not callable(get_state):
+        return None
+
+    try:
+        user_id = get_state("user_id")
+    except Exception:
+        return None
+
+    return user_id if isinstance(user_id, str) and user_id else None
+
+
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Broad exception handling around ctx.get_state may hide programming/configuration issues

This helper currently catches all `Exception` from `ctx.get_state("user_id")` and returns `None`, which can mask real context/configuration bugs (e.g., attribute/type errors). Please narrow the `except` to the specific failure modes you expect from `get_state`, or at least log the exception (e.g., at debug level), so genuine integration issues remain visible while still tolerating missing state in normal operation.

Suggested implementation:

```python
import logging

from starlette.requests import Request

```

```python
_MAX_POLL_SECONDS = 600

logger = logging.getLogger(__name__)

```

```python
def get_user_id_from_context(ctx: Context) -> str | None:
    """Read user_id from request-scoped context in remote-hosted mode."""
    if not config.http_remote_hosted:
        return None

    get_state = getattr(ctx, "get_state", None)
    if not callable(get_state):
        return None

    try:
        user_id = get_state("user_id")
    except KeyError:
        # Expected case: user_id state is not present
        return None
    except Exception as exc:
        # Log unexpected integration/configuration issues while remaining tolerant
        logger.debug("Failed to read 'user_id' from context state", exc_info=exc)
        return None

    return user_id if isinstance(user_id, str) and user_id else None

```

If `ctx.get_state` can raise more specific, known exceptions (e.g. a custom context error), you should replace or augment the generic `Exception` handler with those specific types so that truly unexpected errors can still propagate.
</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.

Comment on lines +396 to +400
if not registry_tools and not self._has_logged_empty_registry_warning:
logger.warning(
"Tool registry is empty during tool-list filtering; treating tools as unknown/visible."
)
self._has_logged_empty_registry_warning = True
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (bug_risk): Empty registry warning message does not match actual behavior when previous metadata exists

When registry_tools is empty you log that tools are treated as unknown/visible, but the existing _unity_managed_tool_names, _tool_alias_to_unity_target, and _server_only_tool_names remain unchanged. If the registry was previously populated, tools still follow the old metadata. Please either update the log message to state that existing metadata is retained, or clear these metadata structures when the registry is empty so behavior matches the message.

Comment on lines +32 to +41
def get_user_id_from_context(ctx: Context) -> str | None:
"""Read user_id from request-scoped context in remote-hosted mode."""
if not config.http_remote_hosted:
return None

get_state = getattr(ctx, "get_state", None)
if not callable(get_state):
return None

try:
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (bug_risk): Broad exception handling around ctx.get_state may hide programming/configuration issues

This helper currently catches all Exception from ctx.get_state("user_id") and returns None, which can mask real context/configuration bugs (e.g., attribute/type errors). Please narrow the except to the specific failure modes you expect from get_state, or at least log the exception (e.g., at debug level), so genuine integration issues remain visible while still tolerating missing state in normal operation.

Suggested implementation:

import logging

from starlette.requests import Request
_MAX_POLL_SECONDS = 600

logger = logging.getLogger(__name__)
def get_user_id_from_context(ctx: Context) -> str | None:
    """Read user_id from request-scoped context in remote-hosted mode."""
    if not config.http_remote_hosted:
        return None

    get_state = getattr(ctx, "get_state", None)
    if not callable(get_state):
        return None

    try:
        user_id = get_state("user_id")
    except KeyError:
        # Expected case: user_id state is not present
        return None
    except Exception as exc:
        # Log unexpected integration/configuration issues while remaining tolerant
        logger.debug("Failed to read 'user_id' from context state", exc_info=exc)
        return None

    return user_id if isinstance(user_id, str) and user_id else None

If ctx.get_state can raise more specific, known exceptions (e.g. a custom context error), you should replace or augment the generic Exception handler with those specific types so that truly unexpected errors can still propagate.

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