Skip to content

Conversation

@msanatan
Copy link
Member

@msanatan msanatan commented Jan 21, 2026

This is a setting on the MCP server that changes the behavior of custom tools. Since v8, custom tools have been executable via another function tool, and available via a resource. This is needed when one MCP server is used for multiple Unity projects, as custom tools are defined on a project level.

However, for many it's fine for the custom tool to be available globally like the other tools (i.e. how they were in v7).

We add a CLI arg to toggle between these 2 different ways to handle custom tools

Summary by Sourcery

Add configurable scoping for custom tools so they can be either project-specific or globally registered, and wire this through the server startup, plugin hub, and Unity editor HTTP launch flow.

New Features:

  • Introduce a project-scoped tools mode toggleable via CLI flag, environment variable, and Unity editor UI to control how custom tools are exposed.
  • Enable global registration of custom tools as standard MCP tools when project scoping is disabled, with automatic handler generation and telemetry/logging integration.

Enhancements:

  • Refactor MCP server initialization into a factory that configures instructions, tools, resources, middleware, and routes based on the project-scoped tools setting.
  • Adjust resource and tool auto-registration to conditionally include the custom tools resource and execute_custom_tool helper depending on the scoping mode.
  • Update startup instructions to describe the current custom tools behavior according to the selected scoping mode.

Summary by CodeRabbit

  • New Features

    • Project-scoped tools support so custom tools can be limited to individual projects (enabled by default).
    • New toggle in Connection settings to enable/disable project-scoped tools; preference is persisted.
    • Server adds a CLI option to enable/disable project-scoped tools at startup; server/tool registration respects the preference.
  • Tests

    • Added metadata for a Unity edit-mode test asset.

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

…on behavior

Add `--project-scoped-tools` CLI flag and `UNITY_MCP_PROJECT_SCOPED_TOOLS` environment variable to control whether custom tools are registered globally or scoped to specific Unity projects.

Closes CoplayDev#416
Add UI toggle in Connection section to control project-scoped tools flag when using HTTP Local transport. The toggle:
- Defaults to enabled (true)
- Persists state in EditorPrefs
- Only displays when HTTP Local transport is selected
- Automatically appends `--project-scoped-tools` flag to uvx server command
- Updates manual config display when toggled
@msanatan msanatan self-assigned this Jan 21, 2026
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Jan 21, 2026

Reviewer's Guide

Adds a configurable "project scoped tools" mode that controls whether custom Unity MCP tools remain per-project via resources/function tools or are registered as global MCP tools, wiring this through server startup, CLI/env flags, HTTP tooling registration, and the Unity editor UI/launch command.

Sequence diagram for global custom tool registration and execution

sequenceDiagram
    participant UnityEditor
    participant UnityPlugin as UnityPluginHubClient
    participant PluginHub
    participant CustomToolService
    participant FastMCP

    UnityEditor->>UnityPlugin: Register custom tools for project
    UnityPlugin->>PluginHub: send RegisterToolsMessage(session_id, tools)
    PluginHub->>CustomToolService: register_global_tools(tools)
    CustomToolService->>CustomToolService: _register_global_tool for each tool
    CustomToolService->>FastMCP: mcp.tool(name, description)(handler)
    FastMCP-->>CustomToolService: global tool registered

    actor MCPClient
    MCPClient->>FastMCP: invoke global custom tool with params
    FastMCP->>CustomToolService: global handler(ctx, kwargs)
    CustomToolService->>CustomToolService: resolve_project_id_for_unity_instance
    CustomToolService->>CustomToolService: execute_tool(project_id, tool_name, unity_instance, params)
    CustomToolService-->>FastMCP: MCPResponse
    FastMCP-->>MCPClient: tool result
Loading

Class diagram for updated CustomToolService project-scoped vs global tools

classDiagram
    class FastMCP
    class Context

    class ToolParameterModel {
        +string name
        +string type
        +bool required
        +string default_value
    }

    class ToolDefinitionModel {
        +string name
        +string description
        +List~ToolParameterModel~ parameters
    }

    class MCPResponse {
        +bool success
        +string message
    }

    class CustomToolService {
        -FastMCP _mcp
        -bool _project_scoped_tools
        -dict~string, dict~string, ToolDefinitionModel~~ _project_tools
        -dict~string, string~ _hash_to_project
        -dict~string, ToolDefinitionModel~ _global_tools
        -static CustomToolService _instance
        +CustomToolService(mcp FastMCP, project_scoped_tools bool)
        +get_instance() CustomToolService
        +register_tools(request Request) JSONResponse
        +execute_tool(project_id string, tool_name string, unity_instance string, params dict~string, object~) MCPResponse
        +register_global_tools(tools List~ToolDefinitionModel~) void
        -_register_project_tools(project_id string, tools List~ToolDefinitionModel~, project_hash string) tuple~List~string~, List~string~~
        -_register_global_tool(definition ToolDefinitionModel) void
        -_build_global_tool_handler(definition ToolDefinitionModel) function
        -_build_signature(definition ToolDefinitionModel) Signature
        -_build_annotations(definition ToolDefinitionModel) dict~string, object~
        -_map_param_type(param ToolParameterModel) object
        -_coerce_default(value string, param_type string) object
        -_register_tool(project_id string, tool ToolDefinitionModel) void
        -_is_registered(project_id string, tool_name string) bool
        -_safe_response(response object) dict~string, string~
    }

    CustomToolService --> FastMCP : uses
    CustomToolService --> ToolDefinitionModel : registers
    ToolDefinitionModel --> ToolParameterModel : has
    CustomToolService --> MCPResponse : returns
    CustomToolService --> Context : global handler ctx param
Loading

Architecture flow diagram for project_scoped_tools true vs false

flowchart TD
    UE[Unity Editor]
    PL[Unity Plugin]
    SV[MCP Server]
    CTS[CustomToolService]
    RES[custom_tools resource]
    EXECTOOL[execute_custom_tool tool]
    GLOBALTOOLS[Global custom tools]

    UE --> PL
    PL -->|Launch server with flag| SV
    SV --> CTS

    CTS -->|project_scoped_tools true| RES
    CTS -->|project_scoped_tools true| EXECTOOL

    CTS -->|project_scoped_tools false| GLOBALTOOLS

    subgraph Project_scoped_mode
        direction LR
        CTS -. registers .-> RES
        CTS -. uses .-> EXECTOOL
    end

    subgraph Global_mode
        direction LR
        PL -->|RegisterToolsMessage| CTS
        CTS -. register_global_tools .-> GLOBALTOOLS
    end
Loading

File-Level Changes

Change Details Files
Introduce project-scoped vs global custom tools behavior in CustomToolService and global tool registration via PluginHub.
  • Extend CustomToolService to accept a project_scoped_tools flag and track global tool definitions alongside project-scoped tools.
  • Refactor project tool registration into a helper that can also register tools globally when project scoping is disabled.
  • Add APIs to register global tools, build FastMCP tool handlers for them from ToolDefinitionModel, and map parameter metadata into Python call signatures, annotations, and default values.
  • On plugin tool registration, attempt to register tools globally via the CustomToolService singleton, failing softly if unavailable.
Server/src/services/custom_tool_service.py
Server/src/transport/plugin_hub.py
Make MCP server construction and startup configurable for project-scoped tools via CLI flag and environment variable, and adjust instructions and resource/tool registration accordingly.
  • Extract MCP server construction into a create_mcp_server function that accepts a project_scoped_tools flag and wires up routes, middleware, tools, and resources based on it.
  • Change instructions text to describe either project-scoped custom tools via resource or globally registered tools depending on configuration.
  • Add a --project-scoped-tools CLI flag and UNITY_MCP_PROJECT_SCOPED_TOOLS environment variable control, persisting the effective value back to the environment.
  • Update register_all_tools and register_all_resources to conditionally skip execute_custom_tool and custom_tools when project-scoped tools are disabled.
Server/src/main.py
Server/src/services/resources/__init__.py
Server/src/services/tools/__init__.py
Expose project-scoped tools configuration in the Unity editor UI and propagate it into the local HTTP server launch command.
  • Add UI elements (row + toggle) for project-scoped tools to the connection section window, initialize from EditorPrefs, and react to changes by refreshing HTTP configuration and notifying listeners.
  • Control visibility of the new project-scoped tools row based on transport mode and whether HTTP local is selected.
  • Persist the project-scoped tools preference under a new EditorPrefs key and surface it in the EditorPrefs window.
  • Include a --project-scoped-tools flag in the generated local HTTP server uvx command when the preference is enabled.
MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs
MCPForUnity/Editor/Services/ServerManagementService.cs
MCPForUnity/Editor/Constants/EditorPrefKeys.cs
MCPForUnity/Editor/Windows/EditorPrefs/EditorPrefsWindow.cs
MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.uxml

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 Jan 21, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds a Project-Scoped Tools EditorPref and UI toggle, propagates it into local server launch args, and updates the server to support project-scoped vs. global tool/resource registration and exposure based on that flag.

Changes

Cohort / File(s) Summary
Editor Pref Key & Wiring
MCPForUnity/Editor/Constants/EditorPrefKeys.cs, MCPForUnity/Editor/Windows/EditorPrefs/EditorPrefsWindow.cs, MCPForUnity/Editor/Services/ServerManagementService.cs
Added ProjectScopedToolsLocalHttp EditorPref constant, registered it in known pref types, and ServerManagementService reads it to append --project-scoped-tools to local server commands.
Editor UI
MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.cs, MCPForUnity/Editor/Windows/Components/Connection/McpConnectionSection.uxml
Added a Project-Scoped Tools UI row and toggle, persisted to EditorPrefs, added visibility logic tied to HTTP/local selection, and refresh callbacks to update displayed server command.
Server: entry & MCP construction
Server/src/main.py
Added --project-scoped-tools CLI flag, refactored MCP creation into create_mcp_server(project_scoped_tools) and _build_instructions, and moved route/middleware/tool/resource registration into MCP construction using the flag.
Server: custom tool service
Server/src/services/custom_tool_service.py
CustomToolService gains project_scoped_tools flag; added global-tool registration APIs and dynamic handler/signature generation; reorganized project vs. global registration flow and added supporting helpers.
Server: tools & resources registration
Server/src/services/tools/__init__.py, Server/src/services/resources/__init__.py
register_all_tools and register_all_resources accept *, project_scoped_tools: bool = True and skip registering custom-tools endpoint/resource when false (with logging).
Server transport integration
Server/src/transport/plugin_hub.py
After per-session tool registration, attempts to call CustomToolService.register_global_tools() for newly registered tools; errors are logged and ignored.
Tests / Assets
TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/UIDocumentSerializationTests.cs.meta
Added Unity .meta file for a test asset.

Sequence Diagram(s)

sequenceDiagram
    participant Editor as Editor UI
    participant Prefs as EditorPrefs
    participant ServerMgr as ServerManagementService
    participant LocalServer as MCP Process
    participant CustomTool as CustomToolService

    Editor->>Prefs: Set ProjectScopedToolsLocalHttp
    Editor->>ServerMgr: Request refreshed launch command
    ServerMgr->>Prefs: Read ProjectScopedToolsLocalHttp
    ServerMgr->>LocalServer: Launch with/without --project-scoped-tools
    LocalServer->>CustomTool: Initialize(project_scoped_tools)
    alt project_scoped_tools == true
        CustomTool->>LocalServer: Register tools per-project
        LocalServer->>LocalServer: Register custom_tools resource
    else project_scoped_tools == false
        CustomTool->>LocalServer: Register tools globally
        LocalServer->>LocalServer: Skip custom_tools resource
    end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested Reviewers

  • dsarno

Poem

🐰 I nudged a toggle, soft and small,

Local or global — you pick the call.
Editor saves, the server spins anew,
Tools find their homes, tidy and true.
Hoppity code, a nibble of glue.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Project scoped tools' is directly related to the main feature being added—a new setting to toggle between project-scoped and global custom tool registration modes.
Description check ✅ Passed The PR description covers the motivation, solution, and references the Sourcery summary of changes. However, it lacks explicit sections for 'Type of Change', 'Changes Made', 'Testing/Screenshots', and 'Related Issues' as specified in the template.

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

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@msanatan
Copy link
Member Author

Screenshot 2026-01-20 at 8 51 00 PM

Works well, this closes #416

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, and left some high level feedback:

  • Moving custom_tool_service = CustomToolService(mcp, ...) from module import time into create_mcp_server changes when the global is defined; if other modules reference main.custom_tool_service during import or before main() runs, this will now be NameError/None, so consider either keeping a module-level initialization or replacing the global with an accessor (e.g., via CustomToolService.get_instance()) to avoid lifecycle issues.
  • The truthy parsing for --project-scoped-tools and UNITY_MCP_PROJECT_SCOPED_TOOLS (("1", "true", "yes", "on")) is embedded directly in main; consider extracting this into a small helper so any future flags/env vars that need the same semantics can share a single, tested implementation.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Moving `custom_tool_service = CustomToolService(mcp, ...)` from module import time into `create_mcp_server` changes when the global is defined; if other modules reference `main.custom_tool_service` during import or before `main()` runs, this will now be `NameError`/`None`, so consider either keeping a module-level initialization or replacing the global with an accessor (e.g., via `CustomToolService.get_instance()`) to avoid lifecycle issues.
- The truthy parsing for `--project-scoped-tools` and `UNITY_MCP_PROJECT_SCOPED_TOOLS` (`("1", "true", "yes", "on")`) is embedded directly in `main`; consider extracting this into a small helper so any future flags/env vars that need the same semantics can share a single, tested implementation.

## Individual Comments

### Comment 1
<location> `Server/src/services/custom_tool_service.py:398` </location>
<code_context>
+            return dict
+        return str
+
+    def _coerce_default(self, value: str | None, param_type: str):
+        if value is None:
+            return None
</code_context>

<issue_to_address>
**suggestion:** Allow `_coerce_default`’s `param_type` annotation to reflect the actual usage.

Since `_build_signature` passes `param.type`, which may be `None`, the current annotation `param_type: str` doesn’t match actual usage. Please update it to `param_type: str | None` (or `Optional[str]`) so the signature reflects the accepted inputs and stays consistent with the internal `param_type or "string"` guard.

```suggestion
    def _coerce_default(self, value: str | None, param_type: str | None):
```
</issue_to_address>

### Comment 2
<location> `Server/src/transport/plugin_hub.py:323-324` </location>
<code_context>
+
+            service = CustomToolService.get_instance()
+            service.register_global_tools(payload.tools)
+        except Exception as exc:
+            logger.debug(
+                "Skipping global custom tool registration: %s", exc)
+
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Broadened exception handling for global tool registration may hide real configuration errors.

Catching `Exception` here will also hide programming bugs in `get_instance`, `register_global_tools`, or access to `payload.tools`, and only log them at debug level. Consider narrowing this to the specific expected failures (e.g., initialization/lookup errors) and logging any truly unexpected exceptions at warning level so operational issues are surfaced.

Suggested implementation:

```python
        logger.info(
            f"Registered {len(payload.tools)} tools for session {session_id}")

        try:
            from services.custom_tool_service import CustomToolService

            service = CustomToolService.get_instance()
            service.register_global_tools(payload.tools)
        except (ImportError, AttributeError) as exc:
            # Expected issues such as missing service or tools attribute;
            # skip global registration quietly in these cases.
            logger.debug(
                "Skipping global custom tool registration due to expected error: %s",
                exc,
            )
        except Exception as exc:
            # Unexpected errors may indicate real configuration or programming issues
            logger.warning(
                "Unexpected error during global custom tool registration; "
                "custom tools may not be available globally",
                exc_info=exc,
            )

    async def _handle_command_result(self, payload: CommandResultMessage) -> None:

```

If `CustomToolService.get_instance()` or `register_global_tools` raise specific, known exception types (e.g., `CustomToolInitializationError`, `ToolRegistryError`), you should also add those to the expected-exception tuple in the first `except` clause so they are treated as anticipated failures rather than escalated as warnings.
</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: 1

Caution

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

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

49-62: Tool count in log message may be inaccurate when skipping tools.

When execute_custom_tool is skipped, the final log at line 62 still reports len(tools) which includes the skipped tool. For accuracy, consider tracking the actual registered count like resources/__init__.py does with registered_count.

🔧 Suggested fix
+    registered_count = 0
     for tool_info in tools:
         func = tool_info['func']
         tool_name = tool_info['name']
         description = tool_info['description']
         kwargs = tool_info['kwargs']

         if not project_scoped_tools and tool_name == "execute_custom_tool":
             logger.info(
                 "Skipping execute_custom_tool registration (project-scoped tools disabled)")
             continue

         # Apply the `@mcp.tool` decorator, telemetry, and logging
         wrapped = log_execution(tool_name, "Tool")(func)
         wrapped = telemetry_tool(tool_name)(wrapped)
         wrapped = mcp.tool(
             name=tool_name, description=description, **kwargs)(wrapped)
         tool_info['func'] = wrapped
         logger.debug(f"Registered tool: {tool_name} - {description}")
+        registered_count += 1

-    logger.info(f"Registered {len(tools)} MCP tools")
+    logger.info(f"Registered {registered_count} MCP tools")
🤖 Fix all issues with AI agents
In `@Server/src/main.py`:
- Around line 428-432: The CLI currently only defines
parser.add_argument("--project-scoped-tools", ...) which can turn project-scoped
tools on but cannot override an environment variable to turn it off; add a
complementary flag (e.g., parser.add_argument("--global-tools",
action="store_true", ...)) and place both flags into a mutually exclusive
argparse group (or implement explicit three-state parsing) and update the logic
that reads UNITY_MCP_PROJECT_SCOPED_TOOLS to allow the CLI flag to override the
env var in both directions so a user can explicitly enable or disable
project-scoped tools for a single run.
🧹 Nitpick comments (2)
Server/src/transport/plugin_hub.py (1)

318-325: Consider catching more specific exceptions.

The broad Exception catch (flagged by static analysis) could mask unexpected errors. Since CustomToolService.get_instance() raises RuntimeError when uninitialized, and other failures are likely AttributeError or import-related, consider narrowing the scope.

♻️ Suggested improvement
         try:
             from services.custom_tool_service import CustomToolService
 
             service = CustomToolService.get_instance()
             service.register_global_tools(payload.tools)
-        except Exception as exc:
+        except (RuntimeError, ImportError) as exc:
             logger.debug(
                 "Skipping global custom tool registration: %s", exc)

That said, the current implementation is functionally correct for optional/fallback behavior—this is a minor refinement.

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

131-137: Consider removing defensive null checks if the UXML update is guaranteed.

If the new elements are now required, it may be cleaner to rely on initialization/tests and let failures surface rather than guarding each access. If older UXML compatibility is still a requirement, consider an explicit upgrade note or test coverage for the fallback path. Based on learnings, prefer relying on the established testing process over defensive null-checks in Editor windows.

♻️ Optional simplification (if older UXML is no longer supported)
-            if (projectScopedToolsToggle != null)
-            {
-                projectScopedToolsToggle.value = EditorPrefs.GetBool(
-                    EditorPrefKeys.ProjectScopedToolsLocalHttp,
-                    true
-                );
-            }
+            projectScopedToolsToggle.value = EditorPrefs.GetBool(
+                EditorPrefKeys.ProjectScopedToolsLocalHttp,
+                true
+            );
-            if (projectScopedToolsToggle != null)
-            {
-                projectScopedToolsToggle.RegisterValueChangedCallback(evt =>
-                {
-                    EditorPrefs.SetBool(EditorPrefKeys.ProjectScopedToolsLocalHttp, evt.newValue);
-                    UpdateHttpServerCommandDisplay();
-                    OnManualConfigUpdateRequested?.Invoke();
-                });
-            }
+            projectScopedToolsToggle.RegisterValueChangedCallback(evt =>
+            {
+                EditorPrefs.SetBool(EditorPrefKeys.ProjectScopedToolsLocalHttp, evt.newValue);
+                UpdateHttpServerCommandDisplay();
+                OnManualConfigUpdateRequested?.Invoke();
+            });
-            if (projectScopedToolsRow == null)
-            {
-                return;
-            }
-
             bool useHttp = transportDropdown != null && (TransportProtocol)transportDropdown.value != TransportProtocol.Stdio;
             bool httpLocalSelected = IsHttpLocalSelected();
             projectScopedToolsRow.style.display = useHttp && httpLocalSelected
                 ? DisplayStyle.Flex
                 : DisplayStyle.None;

Also applies to: 246-254, 485-490

msanatan and others added 3 commits January 21, 2026 10:44
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
…conversion

Remove unnecessary environment variable conversion for project_scoped_tools flag.
…ation

Split exception handling to distinguish between expected RuntimeError (service not initialized) and unexpected errors.
@msanatan msanatan merged commit b69ee80 into CoplayDev:main Jan 21, 2026
1 of 2 checks passed
@msanatan msanatan deleted the project-scoped-tools branch January 21, 2026 17:07
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@Server/src/main.py`:
- Around line 326-334: The code currently manipulates FastMCP internals
(mcp._get_additional_http_routes and mcp._additional_http_routes) to register a
WebSocketRoute for PluginHub; instead, stop touching private attributes and
register the endpoint via FastMCP's public integration point: either mount a
WebSocket route that delegates to mcp.websocket_endpoint at "/hub/plugin" or
expose PluginHub through the FastMCP app by mounting the FastMCP ASGI app in the
outer ASGI layer. Concretely, remove uses of _get_additional_http_routes and
_additional_http_routes, and replace with logic that, when HTTP/WebSocket
transport is active, wires "/hub/plugin" to mcp.websocket_endpoint (or mounts
the whole mcp app) so PluginHub behavior is reached through the documented
public API (refer to symbols: mcp, PluginHub, WebSocketRoute,
mcp.websocket_endpoint).

Comment on lines +326 to 334
# Mount plugin websocket hub at /hub/plugin when HTTP transport is active
existing_routes = [
route for route in mcp._get_additional_http_routes()
if isinstance(route, WebSocketRoute) and route.path == "/hub/plugin"
]
if not existing_routes:
mcp._additional_http_routes.append(
WebSocketRoute("/hub/plugin", PluginHub))

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd Server && head -n 350 src/main.py | tail -n +290

Repository: CoplayDev/unity-mcp

Length of output: 2370


🏁 Script executed:

cd Server && rg "_additional_http_routes|_get_additional_http_routes" --type py -B 2 -A 2

Repository: CoplayDev/unity-mcp

Length of output: 543


🌐 Web query:

FastMCP WebSocket route public API documentation

💡 Result:

Here are the FastMCP WebSocket / public-route docs and related resources:

  • FastMCP WebSocket + route mapping and OpenAPI integration (official docs). Covers WebSocket endpoint usage, route mapping and public routes. [1]
  • FastMCP GitHub repository (API reference, examples, websocket endpoint implementation and usage). [2]
  • MCPSock (Python WebSocket client/server for FastMCP) — installation and examples for WebSocket routes and handlers. [3]

If you want, I can open any of those pages and extract the exact WebSocket route API examples or copy the relevant code snippets. Which page should I pull details from?


🏁 Script executed:

cd Server && grep -r "class FastMCP" --include="*.py" | head -5

Repository: CoplayDev/unity-mcp

Length of output: 45


🏁 Script executed:

cd Server && python3 << 'EOF'
import inspect
try:
    from fastmcp import FastMCP
    # List all public methods of FastMCP
    public_methods = [m for m in dir(FastMCP) if not m.startswith('_')]
    print("FastMCP public methods:")
    for method in sorted(public_methods):
        print(f"  {method}")
    
    # Check if there's a method related to routes or websocket
    route_methods = [m for m in public_methods if 'route' in m.lower() or 'websocket' in m.lower()]
    print("\nRoute/WebSocket related methods:")
    for method in route_methods:
        print(f"  {method}")
except Exception as e:
    print(f"Error: {e}")
EOF

Repository: CoplayDev/unity-mcp

Length of output: 149


🌐 Web query:

FastMCP public API add_websocket_route custom_route documentation

💡 Result:

Short answer:

  • FastMCP exposes a decorator FastMCP.custom_route(...) to register normal HTTP routes on the server (health checks, OAuth callbacks, small admin APIs). Use @mcp.custom_route(path, methods=[...]) with an async Starlette Request handler. [1][2]

  • There is no public helper named add_websocket_route in the FastMCP docs. To add a WebSocket endpoint, mount FastMCP’s websocket endpoint into an ASGI app (e.g., Starlette/FastAPI) using a WebSocketRoute that points at mcp.websocket_endpoint, or mount the FastMCP app and add WebSocket routes in the outer ASGI app. Example patterns are shown in the docs. [3][1]

References

  • FastMCP server API / custom_route docs. [1]
  • Running/HTTP deployment and custom routes. [2]
  • WebSocket integration example (mount WebSocketRoute -> mcp.websocket_endpoint). [3]

Coupling to FastMCP private API and undocumented WebSocket pattern.

The code accesses mcp._get_additional_http_routes() and mcp._additional_http_routes directly—these are implementation details that FastMCP may change without notice. Additionally, FastMCP's documented approach for WebSocket endpoints is to mount a WebSocketRoute pointing to mcp.websocket_endpoint, not append custom handlers to internal lists.

Replace this pattern with the documented public approach: mount the WebSocket route through FastMCP's proper integration point (either using mcp.websocket_endpoint or mounting the FastMCP app in the outer ASGI layer). This will ensure compatibility with FastMCP updates and follow the intended architecture.

🤖 Prompt for AI Agents
In `@Server/src/main.py` around lines 326 - 334, The code currently manipulates
FastMCP internals (mcp._get_additional_http_routes and
mcp._additional_http_routes) to register a WebSocketRoute for PluginHub;
instead, stop touching private attributes and register the endpoint via
FastMCP's public integration point: either mount a WebSocket route that
delegates to mcp.websocket_endpoint at "/hub/plugin" or expose PluginHub through
the FastMCP app by mounting the FastMCP ASGI app in the outer ASGI layer.
Concretely, remove uses of _get_additional_http_routes and
_additional_http_routes, and replace with logic that, when HTTP/WebSocket
transport is active, wires "/hub/plugin" to mcp.websocket_endpoint (or mounts
the whole mcp app) so PluginHub behavior is reached through the documented
public API (refer to symbols: mcp, PluginHub, WebSocketRoute,
mcp.websocket_endpoint).

This was referenced Jan 21, 2026
@coderabbitai coderabbitai bot mentioned this pull request Jan 31, 2026
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant