Skip to content

Replace session-global Unity instance selection with explicit routing#981

Open
CharlieHess wants to merge 1 commit intoCoplayDev:betafrom
CharlieHess:explicit-unity-instance-routing
Open

Replace session-global Unity instance selection with explicit routing#981
CharlieHess wants to merge 1 commit intoCoplayDev:betafrom
CharlieHess:explicit-unity-instance-routing

Conversation

@CharlieHess
Copy link

@CharlieHess CharlieHess commented Mar 24, 2026

Summary

set_active_instance works for simple single-editor flows, but it breaks down as soon as multiple Unity editors are connected at the same time. The active instance lives in mutable session state, which means one agent can retarget another agent's calls, and an unbound session can drift onto the wrong editor.

The root problem is that instance selection is modeled as ambient state instead of request data. The server can resolve a target internally, but the MCP surface did not make that routing explicit, and the unbound HTTP endpoint had no safe way to pin a whole session to one editor.

This PR makes instance routing explicit. Unity-backed tools and resources now accept unity_instance, HTTP clients can bind an entire session to /mcp/instance/{Name@hash}, and set_active_instance is demoted to a compatibility fallback instead of the primary multi-editor pattern. The middleware also stops silently selecting a different editor when multiple instances are connected, which makes concurrent-editor workflows predictable and safe.

The test coverage here focuses on the contract reviewers care about: the new tool/resource metadata, bound endpoint behavior, and the failure mode when multiple editors are available but no explicit target is provided.

Summary by Sourcery

Make Unity instance routing explicit and safe across HTTP and stdio transports, replacing implicit session-global selection with per-request and endpoint-bound targeting.

New Features:

  • Expose an instance-bound HTTP MCP endpoint that pins a session to a specific Unity editor via /mcp/instance/{Name@hash}.
  • Add explicit unity_instance targeting to Unity-backed tools and resources, including resource URI query support.
  • Provide compatibility handling so set_active_instance rejects use on instance-bound endpoints and remains a fallback for simple single-editor workflows.

Enhancements:

  • Refine Unity instance middleware to prefer explicit per-call, bound-endpoint, or compatibility defaults over implicit autoselection, and prevent overrides on bound endpoints.
  • Update tool and resource registries to inject an optional unity_instance parameter and annotate unity-targeted resources and tools in their metadata.
  • Tighten stdio legacy instance resolution to require explicit selection when multiple Unity instances are running, instead of auto-selecting the most recent.
  • Improve compatibility with different FastMCP layouts when resolving HTTP request and header dependencies.
  • Adjust tool-list filtering to respect bound Unity instances and ignore stale session-selected instances.
  • Clarify instance-targeting behavior and failure modes in the server instructions and user-facing error messages.

Tests:

  • Add integration and unit tests for inline and bound unity_instance routing, including override rejection on instance-bound endpoints and bound routing behavior.
  • Add tests covering the new tool and resource registry metadata for unity_instance parameters and URI templates.
  • Add tests for the new instance-bound MCP HTTP route wiring and for legacy stdio instance selection behavior when multiple instances exist.
  • Update existing transport characterization tests to reflect user_id-only injection in list_tools and the new filtering behavior.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added instance-bound HTTP endpoints at /mcp/instance/{instance} for direct targeting of specific Unity instances via URL path.
    • Enabled unity_instance parameter for explicit instance selection in function calls as an alternative to set_active_instance.
  • Behavior Changes

    • Instance selection now requires explicit specification when multiple instances are available instead of auto-selecting the most recent.
    • Improved error messages providing clearer guidance on resolving instance ambiguity.
  • Documentation Resources

    • Marked GameObject, Prefab, Tool Groups, and Unity Instances resources as server-only (no longer instance-specific).

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Mar 24, 2026

Reviewer's Guide

Makes Unity instance routing explicit and safer by adding per-call and per-endpoint instance selection, exposing unity_instance on Unity-backed tools/resources, introducing an instance-bound HTTP MCP endpoint, and demoting session-global set_active_instance to a compatibility fallback with adjusted behavior and messaging.

Sequence diagram for per-call unity_instance routing on tools and resources

sequenceDiagram
    actor Client
    participant HttpApp as FastMCPHttpApp
    participant Route as MCP_HTTP_Route
    participant Middleware as UnityInstanceMiddleware
    participant Ctx as MiddlewareContext
    participant Tool as UnityBackedToolOrResource
    participant PluginHub as PluginHub
    participant Unity as UnityEditorInstance

    Client->>HttpApp: Call tool/resource with unity_instance
    HttpApp->>Route: Dispatch MCP request
    Route->>Middleware: on_call / on_read_resource
    Middleware->>Ctx: ctx = context.fastmcp_context
    Middleware->>Middleware: _inject_user_id(ctx)
    Middleware->>Middleware: _inject_bound_unity_instance(ctx)

    Middleware->>Middleware: _inject_unity_instance(context, include_legacy_default=True, allow_autoselect=True)
    activate Middleware
    Middleware->>Middleware: requested = _extract_requested_instance(context)
    alt unity_instance provided
        Middleware->>Middleware: _resolve_instance_value(requested, ctx)
        Middleware->>Ctx: set_state("unity_instance", active_instance)
    else no unity_instance
        Middleware->>Ctx: get_state("bound_unity_instance")
        alt no bound_unity_instance
            Middleware->>Middleware: get_active_instance(ctx)
            Middleware->>Middleware: _maybe_autoselect_instance(ctx)
        end
    end
    deactivate Middleware

    Middleware->>Tool: call_next(context)
    Tool->>Ctx: ctx.get_state("unity_instance")
    alt unity_instance missing and multiple editors
        Tool-->>Client: MCPResponse(error: selection required)
    else unity_instance resolved
        Tool->>PluginHub: Execute with unity_instance
        PluginHub->>Unity: Perform operation
        Unity-->>PluginHub: Result
        PluginHub-->>Tool: Result
        Tool-->>Client: MCPResponse(success)
    end
Loading

Flow diagram for tool and resource registration with optional unity_instance parameter

flowchart TB
    subgraph Registry[Tool and Resource Registration]
        direction TB
        Func[Original tool/resource function]
        CheckParam{Has unity_instance
parameter already?}
        AddParam[add_optional_unity_instance_parameter
wraps function signature
and strips kwarg at call time]
        AppendQuery[append_unity_instance_query_template
adds unity_instance to
resource URI template]
        WrappedFunc[Registered function
with optional unity_instance]
        UpdatedURI["Registered URI
with {?unity_instance}"]
    end

    Func --> CheckParam
    CheckParam -->|Yes| WrappedFunc
    CheckParam -->|No and tool| AddParam
    CheckParam -->|No and resource| AddParam

    AddParam --> WrappedFunc

    WrappedFunc -->|Resource registration| AppendQuery
    AppendQuery --> UpdatedURI

    subgraph Runtime[Runtime behavior]
        CallSite[Client calls tool/resource
with unity_instance]
        UnityMW[UnityInstanceMiddleware
_extract_requested_instance]
        Resolver[_resolve_instance_value]
    end

    CallSite --> WrappedFunc
    WrappedFunc --> UnityMW
    UnityMW --> Resolver
    Resolver --> UnityMW
Loading

File-Level Changes

Change Details Files
Refactor UnityInstanceMiddleware to support explicit per-call and per-endpoint instance routing while separating user_id injection from instance selection.
  • Added helpers to resolve HTTP request objects from FastMCP layouts and extract unity_instance from tool arguments, resource URIs, and bound HTTP path params.
  • Introduced bound_unity_instance resolution and storage, with logic to prevent overriding bound targets via inline unity_instance and to compose routing precedence between requested, bound, legacy default, and autoselect behaviors.
  • Split user_id injection into a dedicated method, updated on_list_tools to rely on user_id plus optional bound instance, and adjusted enabled-tool resolution to accept an explicit active_instance parameter.
Server/src/transport/unity_instance_middleware.py
Server/tests/integration/test_inline_unity_instance.py
Server/tests/test_transport_characterization.py
Expose explicit unity_instance targeting in tool/resource registration metadata and signatures while keeping underlying implementations agnostic.
  • Introduced a unity_targeting helper module that can add an optional keyword-only unity_instance parameter to callables and strip it before invocation, plus URI template augmentation for resources.
  • Updated mcp_for_unity_tool and mcp_for_unity_resource decorators to wrap Unity-targeted tools/resources with the optional unity_instance argument and to append unity_instance to resource URI templates by default, with an opt-out for server-only entries.
  • Extended tests to validate that tools/resources expose the unity_instance parameter and resource registry metadata reflects unity_target and updated URIs correctly.
Server/src/services/registry/unity_targeting.py
Server/src/services/registry/tool_registry.py
Server/src/services/registry/resource_registry.py
Server/tests/test_tool_registry_metadata.py
Server/tests/test_resource_registry_metadata.py
Server/src/services/resources/gameobject.py
Server/src/services/resources/prefab.py
Server/src/services/resources/tool_groups.py
Server/src/services/resources/unity_instances.py
Add and wire an instance-bound HTTP MCP endpoint that pins sessions to a specific Unity instance and update server startup to use uvicorn directly in HTTP mode.
  • Implemented helpers to build and attach a /mcp/instance/{instance:path} route alias that reuses the base MCP endpoint handler without duplicating logic.
  • Wrapped FastMCP HTTP app creation in create_http_app, attaching the bound-route alias and logging its registration, and changed main() HTTP startup to run uvicorn directly with explicit configuration.
  • Added tests that verify bound-path construction and that the alias route is registered alongside the base MCP route.
Server/src/main.py
Server/tests/test_main_http_routes.py
Tighten legacy/stdio Unity connection selection semantics to avoid implicit retargeting when multiple instances are running.
  • Changed UnityConnectionPool._resolve_instance_id to auto-select only when a single instance exists, and otherwise raise a ConnectionError containing explicit guidance and a list of available instances.
  • Added a focused test case asserting that multiple available instances without an explicit selection now produce the new error behavior.
  • Aligned PluginHub and custom-tool messaging to instruct callers to either pass unity_instance explicitly or use set_active_instance.
Server/src/transport/legacy/unity_connection.py
Server/tests/test_transport_characterization.py
Server/tests/test_tool_registry_metadata.py
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
Adjust set_active_instance behavior and error messaging to respect bound endpoints and the new explicit-routing model.
  • Updated set_active_instance to detect a bound_unity_instance in context and fail with a descriptive error instead of changing routing on instance-bound endpoints.
  • Added integration coverage ensuring set_active_instance is rejected when called on a bound endpoint.
  • Updated various error messages across PluginHub and related services to mention passing unity_instance explicitly as a primary option instead of relying solely on set_active_instance.
Server/src/services/tools/set_active_instance.py
Server/tests/integration/test_inline_unity_instance.py
Server/tests/test_transport_characterization.py
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
Improve HTTP-transport compatibility with different FastMCP layouts and routing for auth and binding.
  • Introduced a compatibility-safe import path for FastMCP HTTP request and header dependencies, handling both fastmcp.dependencies and fastmcp.server.dependencies.
  • Adjusted _resolve_user_id_from_request to use the new import pattern while preserving behavior.
  • Added tests to validate the behavior of the new compatibility layer indirectly via middleware and transport tests.
Server/src/transport/unity_instance_middleware.py
Server/src/transport/unity_transport.py

Possibly linked issues

  • #: They address the same bug: resources not getting a unity_instance from context, plus add explicit routing improvements.

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 Mar 24, 2026

📝 Walkthrough

Walkthrough

This PR introduces per-call Unity instance targeting through optional unity_instance parameters injected into tools and resources, creates bound MCP HTTP endpoints at /mcp/instance/{instance:path}, refactors the middleware to separate instance injection concerns, and updates registries to control which tools/resources receive targeting support via metadata.

Changes

Cohort / File(s) Summary
HTTP Endpoint Infrastructure
Server/src/main.py
Added create_http_app(), _attach_bound_mcp_route(), and _build_bound_mcp_path() to support instance-bound endpoints; switched from MCP HTTP transport to uvicorn.run() with dynamic route registration; updated server instructions to reference instance-bound path and explicit parameter passing.
Unity Targeting System
Server/src/services/registry/unity_targeting.py
New module introducing UnityInstanceParameter type and two utility functions: add_optional_unity_instance_parameter() for injecting optional unity_instance kwarg into callables with signature/annotation handling, and append_unity_instance_query_template() for appending/merging query parameters into URI templates.
Registry Metadata & Wrapping
Server/src/services/registry/resource_registry.py, Server/src/services/registry/tool_registry.py
Both registries now accept unity_target: bool = True parameter; when enabled, functions are wrapped via add_optional_unity_instance_parameter() and URIs are expanded with query templates; decorated function and URI stored in registry instead of original.
Middleware Instance Routing
Server/src/transport/unity_instance_middleware.py
Major refactoring splitting _inject_unity_instance() into focused functions (_extract_bound_instance(), _inject_bound_unity_instance(), _extract_requested_instance()); now supports bound endpoints with bound_unity_instance, per-call overrides from message args/query params, with validation to reject simultaneous bound + override scenarios; tool listing updated to pass active_instance explicitly.
Instance Selection & Error Handling
Server/src/transport/legacy/unity_connection.py, Server/src/transport/plugin_hub.py, Server/src/services/tools/set_active_instance.py
When multiple instances exist without explicit selection, now raises ConnectionError with instance list and suggestions instead of auto-selecting; set_active_instance blocked on instance-bound endpoints; error messages across plugin_hub and services updated to reference explicit parameter passing.
Service & Resource Updates
Server/src/services/custom_tool_service.py, Server/src/services/resources/custom_tools.py, Server/src/services/tools/execute_custom_tool.py
Error messages updated to instruct "Pass unity_instance explicitly" as alternative to set_active_instance; registry decorators for documentation resources (gameobject, prefab, tool_groups, unity_instances) set unity_target=False for server-only behavior.
Transport Compatibility
Server/src/transport/unity_transport.py
Added fallback import handling for get_http_headers to support multiple fastMCP versions.
Integration & Unit Tests
Server/tests/integration/test_inline_unity_instance.py, Server/tests/test_main_http_routes.py, Server/tests/test_resource_registry_metadata.py, Server/tests/test_tool_registry_metadata.py, Server/tests/test_transport_characterization.py
New integration tests for bound endpoint behavior and per-call overrides; tests for bound path construction and route registration; registry metadata snapshot tests verifying unity_target behavior and parameter injection; middleware test updates for new context structure.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Middleware as UnityInstanceMiddleware
    participant BoundResolver as Bound Instance<br/>Resolver
    participant RequestResolver as Request Instance<br/>Resolver
    participant LegacyResolver as Legacy<br/>Resolver
    participant Tool as Tool/Resource<br/>Handler

    Client->>Middleware: HTTP Request (with path, args, query)
    Middleware->>BoundResolver: Extract bound_unity_instance<br/>from endpoint path
    alt Bound Endpoint Present
        BoundResolver-->>Middleware: bound_unity_instance=X
    else No Bound Endpoint
        BoundResolver-->>Middleware: bound_unity_instance=None
    end
    
    Middleware->>RequestResolver: Extract requested override<br/>from args or query
    alt Requested Override Present
        RequestResolver-->>Middleware: requested=Y
        alt Both Bound & Requested
            Middleware-->>Client: ValueError: Cannot override<br/>bound instance
        else Only Requested
            Middleware->>Tool: active_instance=Y
        end
    else No Requested Override
        alt Bound Instance Present
            Middleware->>Tool: active_instance=X
        else No Bound Instance
            Middleware->>LegacyResolver: Check legacy defaults<br/>(get_active_instance)
            alt Legacy Default Available
                LegacyResolver-->>Middleware: active_instance=Z
                Middleware->>Tool: active_instance=Z
            else No Default & Multiple Instances
                Middleware-->>Client: ConnectionError: Multiple<br/>instances, pass explicitly
            else No Default & Single Instance
                Middleware->>Tool: active_instance=only
            end
        end
    end
    
    Tool->>Client: Response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 A rabbit hops through bound and free,
Now instances may picked be—
Per-call and path, no more debate,
Clear routes decide the routing fate!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.06% 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 clearly and concisely summarizes the main architectural change: moving from session-global instance selection to explicit, per-request routing. It accurately reflects the primary objective of the PR.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering the problem statement, solution approach, specific changes, and test coverage. However, the required template section 'Type of Change' is not explicitly filled out with a checkbox selection, though the context makes it clear this is a breaking change introducing new features and refactoring.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

  • The FastMCP compatibility helpers (_get_http_request_for_binding and _resolve_user_id_from_request) both implement similar try/fallback import logic; consider centralizing this into a shared utility so future FastMCP layout changes only need to be handled in one place.
  • In append_unity_instance_query_template, the "unity_instance" in uri check may skip adding the query parameter when that substring appears in the path rather than the query template; consider parsing the existing {?...} segment explicitly instead of relying on a broad substring match.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The FastMCP compatibility helpers (`_get_http_request_for_binding` and `_resolve_user_id_from_request`) both implement similar try/fallback import logic; consider centralizing this into a shared utility so future FastMCP layout changes only need to be handled in one place.
- In `append_unity_instance_query_template`, the `"unity_instance" in uri` check may skip adding the query parameter when that substring appears in the path rather than the query template; consider parsing the existing `{?...}` segment explicitly instead of relying on a broad substring match.

## Individual Comments

### Comment 1
<location path="Server/src/transport/unity_instance_middleware.py" line_range="51-58" />
<code_context>
     _unity_instance_middleware = middleware


+def _get_http_request_for_binding():
+    """Resolve the current HTTP request using whichever FastMCP layout is installed."""
+    try:
+        from fastmcp.dependencies import get_http_request
+    except Exception:
+        try:
+            from fastmcp.server.dependencies import get_http_request
+        except Exception as exc:  # pragma: no cover - import-layout compatibility
+            raise RuntimeError("FastMCP HTTP request dependency is unavailable") from exc
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Consider narrowing the broad exception handling around FastMCP imports to avoid masking unrelated errors.

Both `except Exception:` blocks will catch any error in the import path (e.g., `AttributeError`, `SyntaxError`), not just missing modules/attributes, which can hide real `fastmcp` issues. Prefer catching `ImportError` (and `AttributeError` if needed) for the first import, so only layout differences are handled while other problems surface. The second broad `except` can remain as a last-resort guard if you still want that behavior.
</issue_to_address>

### Comment 2
<location path="Server/src/transport/unity_instance_middleware.py" line_range="498-507" />
<code_context>
         """Filter MCP tool listing to the Unity-enabled set when session data is available."""
         try:
-            await self._inject_unity_instance(context)
+            await self._inject_user_id(context.fastmcp_context)
+            bound_instance = await self._inject_bound_unity_instance(
+                context.fastmcp_context,
+            )
         except Exception as exc:
             # Re-raise authentication errors so callers get a proper auth failure
             if isinstance(exc, RuntimeError) and "authentication" in str(exc).lower():
                 raise
             _diag.warning(
-                "on_list_tools: _inject_unity_instance failed (%s: %s), continuing without instance",
+                "on_list_tools: user_id injection failed (%s: %s), continuing without instance",
                 type(exc).__name__, exc,
             )
</code_context>
<issue_to_address>
**suggestion:** The log message in `on_list_tools` omits that `_inject_bound_unity_instance` can also fail, which may make debugging harder.

The `try` wraps both `_inject_user_id` and `_inject_bound_unity_instance`, but the warning only mentions user_id. If `_inject_bound_unity_instance` (or `_resolve_instance_value`) raises, the log will point to the wrong step. Please update the message to reflect both operations or to include which step failed.
</issue_to_address>

### Comment 3
<location path="Server/tests/test_transport_characterization.py" line_range="814-826" />
<code_context>
         assert instance is None


+class TestLegacyUnityConnectionPoolSelection:
+    """Test stdio instance resolution when no explicit target is provided."""
+
+    def test_resolve_instance_id_requires_selection_when_multiple_instances_exist(self):
+        pool = UnityConnectionPool()
+        pool._default_instance_id = None
+        instances = [
+            SimpleNamespace(id="ProjectA@aaa111", name="ProjectA", hash="aaa111", path="/tmp/a", port=6401),
+            SimpleNamespace(id="ProjectB@bbb222", name="ProjectB", hash="bbb222", path="/tmp/b", port=6402),
+        ]
+
+        with pytest.raises(ConnectionError, match="Multiple Unity Editor instances are running"):
+            pool._resolve_instance_id(None, instances)
+
</code_context>
<issue_to_address>
**suggestion (testing):** Extend `TestLegacyUnityConnectionPoolSelection` to cover the single-instance auto-select behavior

The new test only covers the multi-instance error path. There’s also new logic for the single-instance case (`len(instances) == 1`), which logs and returns that instance. Please add a test that:
- Uses `instances` with exactly one item and `instance_identifier=None` while `_default_instance_id` is `None`.
- Asserts `_resolve_instance_id(None, instances)` returns that sole instance.
This will cover the new branch and guard against regressions to the previous “most recent instance” behavior.

```suggestion
class TestLegacyUnityConnectionPoolSelection:
    """Test stdio instance resolution when no explicit target is provided."""

    def test_resolve_instance_id_requires_selection_when_multiple_instances_exist(self):
        pool = UnityConnectionPool()
        pool._default_instance_id = None
        instances = [
            SimpleNamespace(id="ProjectA@aaa111", name="ProjectA", hash="aaa111", path="/tmp/a", port=6401),
            SimpleNamespace(id="ProjectB@bbb222", name="ProjectB", hash="bbb222", path="/tmp/b", port=6402),
        ]

        with pytest.raises(ConnectionError, match="Multiple Unity Editor instances are running"):
            pool._resolve_instance_id(None, instances)

    def test_resolve_instance_id_auto_selects_single_instance(self):
        pool = UnityConnectionPool()
        pool._default_instance_id = None
        instances = [
            SimpleNamespace(id="ProjectA@aaa111", name="ProjectA", hash="aaa111", path="/tmp/a", port=6401),
        ]

        selected = pool._resolve_instance_id(None, instances)

        assert selected is instances[0]
        assert selected.id == "ProjectA@aaa111"
```
</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 +51 to +58
def _get_http_request_for_binding():
"""Resolve the current HTTP request using whichever FastMCP layout is installed."""
try:
from fastmcp.dependencies import get_http_request
except Exception:
try:
from fastmcp.server.dependencies import get_http_request
except Exception as exc: # pragma: no cover - import-layout compatibility
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): Consider narrowing the broad exception handling around FastMCP imports to avoid masking unrelated errors.

Both except Exception: blocks will catch any error in the import path (e.g., AttributeError, SyntaxError), not just missing modules/attributes, which can hide real fastmcp issues. Prefer catching ImportError (and AttributeError if needed) for the first import, so only layout differences are handled while other problems surface. The second broad except can remain as a last-resort guard if you still want that behavior.

Comment on lines +498 to +507
await self._inject_user_id(context.fastmcp_context)
bound_instance = await self._inject_bound_unity_instance(
context.fastmcp_context,
)
except Exception as exc:
# Re-raise authentication errors so callers get a proper auth failure
if isinstance(exc, RuntimeError) and "authentication" in str(exc).lower():
raise
_diag.warning(
"on_list_tools: _inject_unity_instance failed (%s: %s), continuing without instance",
"on_list_tools: user_id injection failed (%s: %s), continuing without instance",
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: The log message in on_list_tools omits that _inject_bound_unity_instance can also fail, which may make debugging harder.

The try wraps both _inject_user_id and _inject_bound_unity_instance, but the warning only mentions user_id. If _inject_bound_unity_instance (or _resolve_instance_value) raises, the log will point to the wrong step. Please update the message to reflect both operations or to include which step failed.

Comment on lines +814 to +826
class TestLegacyUnityConnectionPoolSelection:
"""Test stdio instance resolution when no explicit target is provided."""

def test_resolve_instance_id_requires_selection_when_multiple_instances_exist(self):
pool = UnityConnectionPool()
pool._default_instance_id = None
instances = [
SimpleNamespace(id="ProjectA@aaa111", name="ProjectA", hash="aaa111", path="/tmp/a", port=6401),
SimpleNamespace(id="ProjectB@bbb222", name="ProjectB", hash="bbb222", path="/tmp/b", port=6402),
]

with pytest.raises(ConnectionError, match="Multiple Unity Editor instances are running"):
pool._resolve_instance_id(None, instances)
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Extend TestLegacyUnityConnectionPoolSelection to cover the single-instance auto-select behavior

The new test only covers the multi-instance error path. There’s also new logic for the single-instance case (len(instances) == 1), which logs and returns that instance. Please add a test that:

  • Uses instances with exactly one item and instance_identifier=None while _default_instance_id is None.
  • Asserts _resolve_instance_id(None, instances) returns that sole instance.
    This will cover the new branch and guard against regressions to the previous “most recent instance” behavior.
Suggested change
class TestLegacyUnityConnectionPoolSelection:
"""Test stdio instance resolution when no explicit target is provided."""
def test_resolve_instance_id_requires_selection_when_multiple_instances_exist(self):
pool = UnityConnectionPool()
pool._default_instance_id = None
instances = [
SimpleNamespace(id="ProjectA@aaa111", name="ProjectA", hash="aaa111", path="/tmp/a", port=6401),
SimpleNamespace(id="ProjectB@bbb222", name="ProjectB", hash="bbb222", path="/tmp/b", port=6402),
]
with pytest.raises(ConnectionError, match="Multiple Unity Editor instances are running"):
pool._resolve_instance_id(None, instances)
class TestLegacyUnityConnectionPoolSelection:
"""Test stdio instance resolution when no explicit target is provided."""
def test_resolve_instance_id_requires_selection_when_multiple_instances_exist(self):
pool = UnityConnectionPool()
pool._default_instance_id = None
instances = [
SimpleNamespace(id="ProjectA@aaa111", name="ProjectA", hash="aaa111", path="/tmp/a", port=6401),
SimpleNamespace(id="ProjectB@bbb222", name="ProjectB", hash="bbb222", path="/tmp/b", port=6402),
]
with pytest.raises(ConnectionError, match="Multiple Unity Editor instances are running"):
pool._resolve_instance_id(None, instances)
def test_resolve_instance_id_auto_selects_single_instance(self):
pool = UnityConnectionPool()
pool._default_instance_id = None
instances = [
SimpleNamespace(id="ProjectA@aaa111", name="ProjectA", hash="aaa111", path="/tmp/a", port=6401),
]
selected = pool._resolve_instance_id(None, instances)
assert selected is instances[0]
assert selected.id == "ProjectA@aaa111"

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

🧹 Nitpick comments (2)
Server/src/main.py (1)

959-967: Consider documenting the timeout_graceful_shutdown=0 choice.

The immediate shutdown (timeout_graceful_shutdown=0) may be intentional for a locally-run MCP server, but it means in-flight requests won't complete gracefully. If this is deliberate (e.g., to match stdio transport behavior or simplify Unity integration), a brief inline comment would clarify the intent for future maintainers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Server/src/main.py` around lines 959 - 967, The uvicorn.run call in main.py
sets timeout_graceful_shutdown=0 which forces immediate shutdown and prevents
in-flight requests from completing; add a concise inline comment next to the
uvicorn.run invocation (referencing create_http_app and the uvicorn.run call)
stating the intentional rationale for using 0 (e.g., to match stdio transport
behavior, simplify Unity integration, or avoid blocking on shutdown) and note
any trade-offs so future maintainers understand the choice.
Server/src/services/registry/resource_registry.py (1)

19-19: Document the new unity_target parameter in the docstring.

The new parameter is functional but undocumented. Consider adding it to the Args section for discoverability.

📝 Suggested docstring update
     Args:
         name: Resource name (defaults to function name)
         description: Resource description
+        unity_target: Whether to expose unity_instance parameter for explicit routing.
+            When True (default), adds optional unity_instance kwarg and appends
+            {?unity_instance} to the URI template. Set to False for server-only
+            resources that don't interact with Unity instances.
         **kwargs: Additional arguments passed to `@mcp.resource`()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Server/src/services/registry/resource_registry.py` at line 19, Add an Args
entry for the new parameter unity_target in the docstring of the function/method
in resource_registry.py that declares unity_target: bool = True; describe its
type (bool), default (True), and brief purpose (what enabling/disabling unity
target does) and any side effects or expected values so callers can discover and
use the parameter.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Server/src/services/custom_tool_service.py`:
- Around line 375-377: The error message incorrectly tells callers to "Pass
unity_instance explicitly" even though global custom tools built via
self._mcp.tool(...) use signatures from _build_signature()/_build_annotations()
and do not accept unity_instance; either remove that suggestion and only
instruct callers to use set_active_instance (update the MCPResponse message in
the handler), or implement proper support: add a 'unity_instance' parameter to
the signature/annotations generation (_build_signature/_build_annotations) and
in the handler extract/pop unity_instance from kwargs before forwarding so
multi-editor routing works. Ensure you reference the MCPResponse-producing
handler and the _build_signature/_build_annotations code when making the change.

In `@Server/src/services/tools/set_active_instance.py`:
- Around line 26-27: The call to await get_state("bound_unity_instance") in
set_active_instance.py should be wrapped in a try/except to defensively handle
FastMCP missing-state exceptions: change the bound_instance assignment to call
get_state only if callable(get_state) and catch any exception from await
get_state("bound_unity_instance"), setting bound_instance = None on failure so
the instance resolution fallback can proceed; reference the existing pattern
used elsewhere (services/tools/__init__.py) and the get_state and bound_instance
symbols when locating where to add the try/except.

In `@Server/src/transport/legacy/unity_connection.py`:
- Around line 552-557: The branch that auto-selects when len(instances) == 1 is
using a potentially stale 5s-cached list; before returning instances[0] (and
logging via logger.info), refresh the discovery/source that populates instances
(i.e., call the same discovery/lookup method or explicit refresh used elsewhere)
and re-fetch the instances variable, then re-evaluate the length — only proceed
to logger.info and return instances[0] if the refreshed list still has length ==
1, otherwise fall through to the normal selection/ambiguity handling.

---

Nitpick comments:
In `@Server/src/main.py`:
- Around line 959-967: The uvicorn.run call in main.py sets
timeout_graceful_shutdown=0 which forces immediate shutdown and prevents
in-flight requests from completing; add a concise inline comment next to the
uvicorn.run invocation (referencing create_http_app and the uvicorn.run call)
stating the intentional rationale for using 0 (e.g., to match stdio transport
behavior, simplify Unity integration, or avoid blocking on shutdown) and note
any trade-offs so future maintainers understand the choice.

In `@Server/src/services/registry/resource_registry.py`:
- Line 19: Add an Args entry for the new parameter unity_target in the docstring
of the function/method in resource_registry.py that declares unity_target: bool
= True; describe its type (bool), default (True), and brief purpose (what
enabling/disabling unity target does) and any side effects or expected values so
callers can discover and use the parameter.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f330dd17-15b2-4fa3-9b5b-b4fa043c7090

📥 Commits

Reviewing files that changed from the base of the PR and between 49b749a and 4584270.

📒 Files selected for processing (21)
  • Server/src/main.py
  • Server/src/services/custom_tool_service.py
  • Server/src/services/registry/resource_registry.py
  • Server/src/services/registry/tool_registry.py
  • Server/src/services/registry/unity_targeting.py
  • Server/src/services/resources/custom_tools.py
  • Server/src/services/resources/gameobject.py
  • Server/src/services/resources/prefab.py
  • Server/src/services/resources/tool_groups.py
  • Server/src/services/resources/unity_instances.py
  • Server/src/services/tools/execute_custom_tool.py
  • Server/src/services/tools/set_active_instance.py
  • Server/src/transport/legacy/unity_connection.py
  • Server/src/transport/plugin_hub.py
  • Server/src/transport/unity_instance_middleware.py
  • Server/src/transport/unity_transport.py
  • Server/tests/integration/test_inline_unity_instance.py
  • Server/tests/test_main_http_routes.py
  • Server/tests/test_resource_registry_metadata.py
  • Server/tests/test_tool_registry_metadata.py
  • Server/tests/test_transport_characterization.py

Comment on lines 375 to +377
return MCPResponse(
success=False,
message="No active Unity instance. Call set_active_instance with Name@hash from mcpforunity://instances.",
message="No active Unity instance. Pass unity_instance explicitly or call set_active_instance with Name@hash from mcpforunity://instances.",
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:

# First, let's find and examine the custom_tool_service.py file
find . -name "custom_tool_service.py" -type f

Repository: CoplayDev/unity-mcp

Length of output: 107


🏁 Script executed:

# Check the file size and structure
wc -l Server/src/services/custom_tool_service.py

Repository: CoplayDev/unity-mcp

Length of output: 109


🏁 Script executed:

# Examine the specific lines mentioned (375-377) plus surrounding context
sed -n '360,390p' Server/src/services/custom_tool_service.py

Repository: CoplayDev/unity-mcp

Length of output: 1473


🏁 Script executed:

# Find the _build_global_tool_handler method
rg -n "_build_global_tool_handler" Server/src/services/custom_tool_service.py -A 30

Repository: CoplayDev/unity-mcp

Length of output: 2422


🏁 Script executed:

# Find the _build_signature and _build_annotations methods
rg -n "_build_signature|_build_annotations" Server/src/services/custom_tool_service.py -A 15

Repository: CoplayDev/unity-mcp

Length of output: 1674


Error message misleadingly suggests passing unity_instance for global custom tools.

At lines 375-377, the handler tells users to "Pass unity_instance explicitly," but global custom tools registered via self._mcp.tool(...) have signatures built only from definition.parameters (via _build_signature() and _build_annotations()). The handler attempts to retrieve unity_instance from context, not from kwargs, so callers cannot actually provide it as a parameter. Either expose unity_instance in the signature and filter it from forwarded params (enabling multi-editor routing), or revise the message to only mention the set_active_instance workaround.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Server/src/services/custom_tool_service.py` around lines 375 - 377, The error
message incorrectly tells callers to "Pass unity_instance explicitly" even
though global custom tools built via self._mcp.tool(...) use signatures from
_build_signature()/_build_annotations() and do not accept unity_instance; either
remove that suggestion and only instruct callers to use set_active_instance
(update the MCPResponse message in the handler), or implement proper support:
add a 'unity_instance' parameter to the signature/annotations generation
(_build_signature/_build_annotations) and in the handler extract/pop
unity_instance from kwargs before forwarding so multi-editor routing works.
Ensure you reference the MCPResponse-producing handler and the
_build_signature/_build_annotations code when making the change.

Comment on lines +26 to +27
get_state = getattr(ctx, "get_state", None)
bound_instance = await get_state("bound_unity_instance") if callable(get_state) else None
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:

cat -n Server/src/services/tools/set_active_instance.py | head -40

Repository: CoplayDev/unity-mcp

Length of output: 1833


🏁 Script executed:

rg "get_state" Server/src/services/tools/ -B 2 -A 2 -t py

Repository: CoplayDev/unity-mcp

Length of output: 1749


🏁 Script executed:

rg "Context.get_state\|ctx.get_state" --type py -B 2 -A 2 | head -100

Repository: CoplayDev/unity-mcp

Length of output: 45


🏁 Script executed:

rg "@mcp_for_unity_tool" Server/src/services/tools/ -A 5 --type py | grep -E "(`@mcp_for_unity_tool`|group=)" | head -50

Repository: CoplayDev/unity-mcp

Length of output: 3309


🏁 Script executed:

rg "group\s*=" Server/src/services/tools/ --type py -B 1

Repository: CoplayDev/unity-mcp

Length of output: 3355


🏁 Script executed:

cat -n Server/src/services/tools/__init__.py | sed -n '1,40p'

Repository: CoplayDev/unity-mcp

Length of output: 1788


Add defensive handling for bound_unity_instance state lookup.

Line 27 calls await get_state("bound_unity_instance") without catching exceptions. If FastMCP raises for missing state keys, normal unbound /mcp requests will fail before the instance resolution fallback can run. The codebase already demonstrates the defensive pattern in services/tools/__init__.py—wrap the call in try/except.

Suggested fix
     get_state = getattr(ctx, "get_state", None)
-    bound_instance = await get_state("bound_unity_instance") if callable(get_state) else None
+    try:
+        bound_instance = await get_state("bound_unity_instance") if callable(get_state) else None
+    except Exception:
+        bound_instance = None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Server/src/services/tools/set_active_instance.py` around lines 26 - 27, The
call to await get_state("bound_unity_instance") in set_active_instance.py should
be wrapped in a try/except to defensively handle FastMCP missing-state
exceptions: change the bound_instance assignment to call get_state only if
callable(get_state) and catch any exception from await
get_state("bound_unity_instance"), setting bound_instance = None on failure so
the instance resolution fallback can proceed; reference the existing pattern
used elsewhere (services/tools/__init__.py) and the get_state and bound_instance
symbols when locating where to add the try/except.

Comment on lines +552 to +557
if len(instances) == 1:
logger.info(
"No instance specified, auto-selecting sole instance: %s",
instances[0].id,
)
return instances[0]
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

Refresh discovery before auto-selecting the “sole” instance.

These instances come from a 5-second cache. If a second editor connects inside that window, len(instances) == 1 is stale and this branch still routes implicitly to the previously cached editor—the exact behavior this PR is trying to remove.

🔁 Proposed fix
             else:
+                instances = self.discover_all_instances(force_refresh=True)
                 if len(instances) == 1:
                     logger.info(
                         "No instance specified, auto-selecting sole instance: %s",
                         instances[0].id,
                     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Server/src/transport/legacy/unity_connection.py` around lines 552 - 557, The
branch that auto-selects when len(instances) == 1 is using a potentially stale
5s-cached list; before returning instances[0] (and logging via logger.info),
refresh the discovery/source that populates instances (i.e., call the same
discovery/lookup method or explicit refresh used elsewhere) and re-fetch the
instances variable, then re-evaluate the length — only proceed to logger.info
and return instances[0] if the refreshed list still has length == 1, otherwise
fall through to the normal selection/ambiguity handling.

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