Skip to content

Add per-call unity_instance routing via tool arguments#772

Merged
dsarno merged 1 commit intoCoplayDev:betafrom
dsarno:feature/inline-unity-instance-routing
Feb 18, 2026
Merged

Add per-call unity_instance routing via tool arguments#772
dsarno merged 1 commit intoCoplayDev:betafrom
dsarno:feature/inline-unity-instance-routing

Conversation

@dsarno
Copy link
Collaborator

@dsarno dsarno commented Feb 18, 2026

Summary

Adds per-call unity_instance routing so AI clients can target specific Unity instances on any tool call without requiring set_active_instance first. Supersedes the CLI flags approach from #697.

  • Middleware intercepts unity_instance from tool call arguments, resolves it (Name@hash, hash prefix, or port number in stdio mode), and sets request-scoped state — without persisting to the session
  • set_active_instance extended to also accept port numbers (stdio mode) for consistency
  • Server instructions updated to document both routing approaches (session pinning vs per-call)
  • 18 integration tests covering pop behavior, per-call routing, non-persistence, port/hash/Name@hash resolution, edge cases

How it works

Any tool call can include unity_instance as an extra parameter:

# Route by port (stdio only)
manage_scene(action="get_active", unity_instance="6401")

# Route by hash prefix
manage_scene(action="get_active", unity_instance="abc")

# Route by exact Name@hash
manage_scene(action="get_active", unity_instance="MyGame@abc123")

The middleware pops the key before Pydantic validation, resolves it to a validated instance ID, and sets it in request-scoped state for that call only. Subsequent calls without unity_instance revert to the session-persisted instance (or auto-select).

Design decisions

  • Per-call only, not persistent: Inline unity_instance does NOT change the session default. This avoids confusion when alternating between instances
  • Port resolution via discovery: Port numbers resolve to actual Name@hash by querying the connection pool, not synthetic IDs
  • Backward compatible: Existing set_active_instance workflow unchanged; unity_instance parameter is purely additive

Closes #697

Test plan

  • 18 new integration tests (all passing)
  • Full test suite: 684/684 passing
  • Live smoke tested with two real Unity instances on different ports
  • Verified per-call routing does not persist to session state
  • Verified port numbers, hash prefixes, and Name@hash all resolve correctly

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com

Summary by Sourcery

Add support for per-call Unity instance routing via a unity_instance tool argument while keeping session-pinned routing, and extend instance selection ergonomics across transports.

New Features:

  • Allow specifying a unity_instance parameter on individual tool calls to route them to a specific Unity instance without changing session state.
  • Support resolving unity_instance values from port numbers (stdio), exact Name@hash identifiers, or hash prefixes with clear error messages on failure.
  • Extend the set_active_instance tool to accept port numbers in stdio mode alongside Name@hash and hash prefixes for selecting the active Unity instance.
  • Log guidance when multiple Unity instances are discovered, pointing users to per-call routing or set_active_instance.

Documentation:

  • Document both session-pinned and per-call unity_instance routing options and their usage in the server instructions.

Tests:

  • Add integration test coverage for per-call unity_instance routing behavior, including argument popping, non-persistence, resolution by port/hash/Name@hash, edge cases, and set_active_instance port support.

Summary by CodeRabbit

  • New Features

    • Per-call Unity instance routing: specify a target instance for individual calls without changing the session.
    • Port-number targeting for stdio mode (in addition to Name@hash and hash prefixes).
  • Bug Fixes / Validation

    • Rejects per-command instance overrides inside batch calls; require outer-level instance routing.
  • Tests

    • Added integration tests covering per-call routing, port resolution, prefix matching, and edge cases.
  • Documentation

    • Updated guidance for managing multiple connected Unity instances and routing options.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Feb 18, 2026

Reviewer's Guide

Implements per-call Unity instance routing via a new unity_instance tool argument, including middleware-level resolution (port/hash/Name@hash), non-persistent request-scoped routing, extended set_active_instance semantics for port numbers, updated server instructions, and a comprehensive integration test suite validating the behavior and edge cases.

Sequence diagram for per-call unity_instance routing via middleware

sequenceDiagram
    actor Client
    participant MCPServer
    participant UnityInstanceMiddleware
    participant Discovery as InstanceDiscovery
    participant ToolHandler

    Client->>MCPServer: CallToolRequest manage_scene(action="get_active", unity_instance="6401")
    MCPServer->>UnityInstanceMiddleware: _inject_unity_instance(context)

    UnityInstanceMiddleware->>UnityInstanceMiddleware: Read context.message.arguments
    UnityInstanceMiddleware->>UnityInstanceMiddleware: Pop unity_instance from arguments
    UnityInstanceMiddleware->>UnityInstanceMiddleware: _resolve_instance_value("6401", ctx)

    alt value is digit and transport is stdio
        UnityInstanceMiddleware->>Discovery: _discover_instances(ctx)
        Discovery-->>UnityInstanceMiddleware: [instances with id, hash, port]
        UnityInstanceMiddleware->>UnityInstanceMiddleware: Match instance by port
        UnityInstanceMiddleware-->>UnityInstanceMiddleware: Return resolved id Name@hash
    else value is digit and transport is http
        UnityInstanceMiddleware-->>UnityInstanceMiddleware: Raise ValueError (port targeting not supported)
    else value is Name@hash or hash prefix
        UnityInstanceMiddleware->>Discovery: _discover_instances(ctx)
        Discovery-->>UnityInstanceMiddleware: [instances with id, hash]
        UnityInstanceMiddleware->>UnityInstanceMiddleware: Match by exact id or hash prefix
        UnityInstanceMiddleware-->>UnityInstanceMiddleware: Return resolved id Name@hash or error
    end

    UnityInstanceMiddleware->>UnityInstanceMiddleware: Set active_instance for this request only
    UnityInstanceMiddleware->>UnityInstanceMiddleware: Fallback to get_active_instance(ctx) if none
    UnityInstanceMiddleware->>UnityInstanceMiddleware: Fallback to _maybe_autoselect_instance(ctx) if still none
    UnityInstanceMiddleware-->>MCPServer: Context with request-scoped active_instance

    MCPServer->>ToolHandler: Invoke manage_scene with validated args
    ToolHandler->>ToolHandler: Use ctx active_instance for Unity routing
    ToolHandler-->>Client: Tool result
Loading

Updated class diagram for Unity instance routing and set_active_instance

classDiagram
    class UnityInstanceMiddleware {
        -lock
        -active_by_key
        +get_session_key(ctx) str
        +get_active_instance(ctx) str
        +set_active_instance(ctx, instance_id str) None
        +clear_active_instance(ctx) None
        +_inject_unity_instance(context MiddlewareContext) None
        +_maybe_autoselect_instance(ctx) str
        +_discover_instances(ctx) list
        +_resolve_instance_value(value str, ctx) str
    }

    class MiddlewareContext {
        +message
        +get_state(key str)
        +set_state(key str, value)
    }

    class CallToolRequestMessage {
        +arguments dict
    }

    class PluginHub {
        +is_configured() bool
        +get_sessions(user_id) PluginSessions
    }

    class PluginSessions {
        +sessions dict
    }

    class UnityConnectionPool {
        +discover_all_instances(force_refresh bool) list
    }

    class SetActiveInstanceTool {
        +set_active_instance(ctx Context, instance str) dict~str, Any~
    }

    class Context {
        +set_state(key str, value)
        +get_state(key str)
    }

    class Config {
        +transport_mode str
        +http_remote_hosted bool
    }

    UnityInstanceMiddleware --> MiddlewareContext : uses
    MiddlewareContext --> CallToolRequestMessage : has
    UnityInstanceMiddleware --> PluginHub : discovers_http_instances
    UnityInstanceMiddleware --> UnityConnectionPool : discovers_stdio_instances
    UnityInstanceMiddleware --> Config : reads

    SetActiveInstanceTool --> Context : uses
    SetActiveInstanceTool --> UnityConnectionPool : discovers_stdio_instances
    SetActiveInstanceTool --> UnityInstanceMiddleware : sets_active_instance
    SetActiveInstanceTool --> Config : reads
Loading

File-Level Changes

Change Details Files
Add middleware support for per-call unity_instance routing with resolution of ports, hashes, and Name@hash identifiers, while keeping routing request-scoped and non-persistent.
  • Introduce _discover_instances to aggregate running Unity instances from PluginHub (HTTP) and stdio connection pool with robust error handling.
  • Implement _resolve_instance_value to normalize user-supplied unity_instance values, supporting port numbers (stdio), exact Name@hash, and unique hash prefixes with clear ValueError messages on failure.
  • Extend _inject_unity_instance to pop unity_instance from tool arguments before Pydantic validation, resolve it via _resolve_instance_value, and set the active instance only for the current request, falling back to session or auto-select when absent.
  • Enhance _maybe_autoselect_instance logging to surface when multiple instances are available and suggest using unity_instance or set_active_instance.
Server/src/transport/unity_instance_middleware.py
Extend set_active_instance tool to accept port numbers in stdio mode and reuse discovery-based resolution for consistent instance targeting.
  • Update tool description and parameter annotations to document support for Name@hash, hash prefixes, and stdio-only port numbers.
  • Pre-validate numeric instance values: in HTTP mode, return a structured error indicating port-based targeting is unsupported; in stdio mode, resolve ports to Name@hash via get_unity_connection_pool().discover_all_instances.
  • On successful port resolution, set the active instance in middleware and return a success payload including resolved instance ID and session key.
Server/src/services/tools/set_active_instance.py
Document dual routing modes (session pinning vs per-call) in server instructions.
  • Clarify that set_active_instance pins routing for the entire session and remains required when multiple instances exist without per-call overrides.
  • Add guidance and examples for using unity_instance as an optional argument on individual tool calls, including Name@hash, hash prefix, and port-based routing semantics.
Server/src/main.py
Add integration tests validating inline unity_instance routing, non-persistence, resolution semantics, and port support in set_active_instance.
  • Introduce DummyMiddlewareContext and _make_middleware helpers to simulate middleware behavior, transport modes, and discovery sources (PluginHub vs pool).
  • Test that unity_instance is removed from tool arguments while leaving other arguments untouched, and that calls without unity_instance are unaffected.
  • Verify per-call routing overrides the session-persisted instance only for that request and does not mutate the stored active instance, including fall-through behavior for None/empty values and resource reads without arguments.
  • Cover port resolution success and failure in stdio vs HTTP modes, exact/unknown Name@hash, unique/ambiguous/no-match hash prefixes, and new set_active_instance port-handling behavior for both transports.
Server/tests/integration/test_inline_unity_instance.py

Assessment against linked issues

Issue Objective Addressed Explanation
#697 Implement a mechanism in stdio transport to deterministically target a specific Unity instance by its MCP port or identifier from the client side, avoiding ambiguous routing in multi-instance setups.
#697 Expose explicit Unity endpoint targeting for stdio mode via documented configuration interfaces (e.g., CLI flags and/or environment variables like --unity-port/--unity-url and UNITY_MCP_UNITY_PORT/UNITY_MCP_UNITY_URL), including documented precedence with existing instance selection, while preserving backward compatibility. The PR introduces per-call unity_instance routing via tool arguments and extends the set_active_instance tool to accept ports, but it explicitly supersedes the CLI flags approach. No new CLI flags or environment variables are added, and precedence between such options and instance selection is not implemented or documented.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 18, 2026

📝 Walkthrough
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.47% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'Add per-call unity_instance routing via tool arguments' directly and clearly summarizes the main change — enabling per-call instance routing through tool arguments without session persistence.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering all template sections: clear summary of changes, type of change (new feature), specific changes made, testing results, documentation updates, and related issues.
Linked Issues check ✅ Passed The PR directly addresses issue #697 objectives by enabling explicit Unity instance targeting via port numbers (stdio), hash prefixes, and Name@hash identifiers for deterministic multi-instance routing without CLI flags.
Out of Scope Changes check ✅ Passed All changes align with the core objective of adding per-call unity_instance routing and extending set_active_instance; middleware discovery, port resolution, per-call argument handling, and integration tests are all in-scope.

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

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

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

❤️ Share

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

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:

  • The port/hash/Name@hash resolution logic is duplicated between _resolve_instance_value in the middleware and the set_active_instance tool; consider centralizing this resolution into a shared helper so behavior and error messages stay consistent over time.
  • The informative logger.info messages in _maybe_autoselect_instance log all available instance IDs when multiple are present; if deployments can have many instances or IDs are sensitive, you may want to truncate or redact this list to keep logs manageable and privacy-safe.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The port/hash/Name@hash resolution logic is duplicated between `_resolve_instance_value` in the middleware and the `set_active_instance` tool; consider centralizing this resolution into a shared helper so behavior and error messages stay consistent over time.
- The informative `logger.info` messages in `_maybe_autoselect_instance` log all available instance IDs when multiple are present; if deployments can have many instances or IDs are sensitive, you may want to truncate or redact this list to keep logs manageable and privacy-safe.

## Individual Comments

### Comment 1
<location> `Server/tests/integration/test_inline_unity_instance.py:347-356` </location>
<code_context>
+def test_set_active_instance_port_stdio(monkeypatch):
</code_context>

<issue_to_address>
**suggestion (testing):** Add a failure-path test for set_active_instance when a port number does not match any instance

The new tests exercise the success and HTTP rejection cases, but not the failure path for an unknown port in stdio mode. Please add a test (similar to `test_port_number_not_found_raises`) that calls `set_active_instance` in stdio mode with a port not in the fake pool and asserts it returns `success: False` with the expected error message, including the `Available: ...` list. This will better lock in the user-facing error behavior and prevent regressions.

Suggested implementation:

```python
# ---------------------------------------------------------------------------
# set_active_instance tool: port number support
# ---------------------------------------------------------------------------

def test_set_active_instance_unknown_port_stdio(monkeypatch):
    """set_active_instance returns failure in stdio mode when the port is not in the instance pool."""
    monkeypatch.setattr(config, "transport_mode", "stdio")
    monkeypatch.setattr(config, "http_remote_hosted", False)

    from types import SimpleNamespace
    from transport.unity_instance_middleware import UnityInstanceMiddleware, set_unity_instance_middleware

    mw = UnityInstanceMiddleware()
    set_unity_instance_middleware(mw)

    # Pool has only port 6401, we'll ask for 6402.
    mw._unity_instance_pool = [
        SimpleNamespace(id="Proj@abc123", hash="abc123", port=6401),
    ]

    # The new tests for the success path should already demonstrate
    # how set_active_instance is invoked; we mirror that here.
    # If set_active_instance is a top-level helper instead of a method,
    # adjust this call to match that helper.
    result = asyncio.run(mw.set_active_instance("6402"))

    assert result["success"] is False
    # Lock in the user-visible error message including the available ports list.
    assert "No Unity instance found listening on port 6402" in result["message"]
    assert "Available: 6401" in result["message"]


def test_set_active_instance_port_stdio(monkeypatch):

```

You will likely need to make these small adjustments to align with the rest of your test file:

1. **Instance pool attribute name**: If the middleware uses a different attribute than `_unity_instance_pool` to store its pool (e.g. `unity_instance_pool` or similar), update that line accordingly.
2. **Invocation of `set_active_instance`**:  
   - If there is a module-level function (e.g. `from tools.inline_unity_instance import set_active_instance`) used by the existing tests, call that instead of `mw.set_active_instance`.  
   - Mirror exactly how `test_set_active_instance_port_stdio` invokes `set_active_instance` (async vs sync, parameter shape, etc.), and only change the port value to one that is *not* in the fake pool (e.g. `"6402"`).
3. **Imports**:  
   - If `SimpleNamespace` and/or `asyncio` are already imported at the top of the file, you can remove the local import of `SimpleNamespace` and the test will still work.
4. **Error text**:  
   - If your existing error message wording or capitalization for the unknown-port path is slightly different (or uses a different prefix before the `Available:` list), update the string assertions to match the actual message, keeping the explicit check that the `Available: ...` portion is present.
</issue_to_address>

### Comment 2
<location> `Server/tests/integration/test_inline_unity_instance.py:177-127` </location>
<code_context>
+# Port number resolution (stdio)
+# ---------------------------------------------------------------------------
+
+def test_port_number_resolves_to_name_hash_stdio(monkeypatch):
+    """Bare port number resolves to the matching Name@hash in stdio mode."""
+    instances = [
+        SimpleNamespace(id="Proj@abc123", hash="abc123", port=6401),
+        SimpleNamespace(id="Other@def456", hash="def456", port=6402),
+    ]
+    mw = _make_middleware(monkeypatch, transport="stdio", pool_instances=instances)
+
+    ctx = DummyContext()
+    ctx.client_id = "client-1"
+    mw_ctx = DummyMiddlewareContext(ctx, arguments={"unity_instance": "6401"})
+
+    asyncio.run(mw._inject_unity_instance(mw_ctx))
+
+    assert ctx.get_state("unity_instance") == "Proj@abc123"
+
+
</code_context>

<issue_to_address>
**suggestion (testing):** Consider adding a test for non-string unity_instance values to exercise the str() casting path

Since `_inject_unity_instance` does `raw_str = str(raw).strip()`, it accepts non-string `unity_instance` values (e.g., integers). All current tests cover only string inputs. Please add a test where `unity_instance` is an `int` (e.g., `6401`) and confirm it behaves like the string case, plus one for a non-numeric type to verify the expected fallback/error behavior. This will lock in the implicit `str()` casting behavior for future refactors.

Suggested implementation:

```python
# ---------------------------------------------------------------------------
# Port number resolution (stdio)
# ---------------------------------------------------------------------------


def test_port_number_resolves_to_name_hash_stdio(monkeypatch):
    """Bare port number resolves to the matching Name@hash in stdio mode."""
    instances = [
        SimpleNamespace(id="Proj@abc123", hash="abc123", port=6401),
        SimpleNamespace(id="Other@def456", hash="def456", port=6402),
    ]
    mw = _make_middleware(monkeypatch, transport="stdio", pool_instances=instances)

    ctx = DummyContext()
    ctx.client_id = "client-1"
    mw_ctx = DummyMiddlewareContext(ctx, arguments={"unity_instance": "6401"})

    asyncio.run(mw._inject_unity_instance(mw_ctx))

    assert ctx.get_state("unity_instance") == "Proj@abc123"


def test_port_number_resolves_to_name_hash_stdio_int(monkeypatch):
    """Int unity_instance is accepted and resolved via str() casting in stdio mode."""
    instances = [
        SimpleNamespace(id="Proj@abc123", hash="abc123", port=6401),
        SimpleNamespace(id="Other@def456", hash="def456", port=6402),
    ]
    mw = _make_middleware(monkeypatch, transport="stdio", pool_instances=instances)

    ctx = DummyContext()
    ctx.client_id = "client-1"
    # Note: unity_instance passed as int, exercising str() casting path
    mw_ctx = DummyMiddlewareContext(ctx, arguments={"unity_instance": 6401})

    asyncio.run(mw._inject_unity_instance(mw_ctx))

    assert ctx.get_state("unity_instance") == "Proj@abc123"


def test_non_string_unity_instance_non_numeric_stdio(monkeypatch):
    """Non-numeric, non-string unity_instance exercises the str() casting fallback path."""
    instances = [
        SimpleNamespace(id="Proj@abc123", hash="abc123", port=6401),
        SimpleNamespace(id="Other@def456", hash="def456", port=6402),
    ]
    mw = _make_middleware(monkeypatch, transport="stdio", pool_instances=instances)

    class CustomUnityInstance:
        def __str__(self) -> str:
            # Use a value that will not resolve to any known instance to hit the fallback path
            return "not-a-port-or-instance-id"

    ctx = DummyContext()
    ctx.client_id = "client-1"
    mw_ctx = DummyMiddlewareContext(
        ctx,
        arguments={"unity_instance": CustomUnityInstance()},
    )

    # Depending on implementation, this may either:
    #   * Leave unity_instance unset, or
    #   * Raise a specific error when resolution fails.
    # We assume resolution failure raises a ValueError; adjust if the implementation differs.
    with pytest.raises(ValueError):
        asyncio.run(mw._inject_unity_instance(mw_ctx))

    # Ensure no unity_instance was set when resolution fails
    assert ctx.get_state("unity_instance") is None


When a tool call includes unity_instance in its arguments, the middleware:

```

1. Ensure `pytest` is imported at the top of the file (e.g., `import pytest`) if it is not already present.
2. If `_inject_unity_instance` uses a different exception type on resolution failure, update `with pytest.raises(ValueError):` to the appropriate exception class to match the actual behavior.
3. If the real behavior on resolution failure is to *not* raise but to silently ignore/clear the value, remove the `pytest.raises(...)` context and instead assert the expected fallback behavior (e.g., `ctx.get_state("unity_instance") is None` and/or that no active instance changes).
</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 +347 to +356
def test_set_active_instance_port_stdio(monkeypatch):
"""set_active_instance accepts a port number in stdio mode and resolves to Name@hash."""
monkeypatch.setattr(config, "transport_mode", "stdio")
monkeypatch.setattr(config, "http_remote_hosted", False)

from transport.unity_instance_middleware import UnityInstanceMiddleware, set_unity_instance_middleware
mw = UnityInstanceMiddleware()
set_unity_instance_middleware(mw)

pool_instance = SimpleNamespace(id="Proj@abc123", hash="abc123", port=6401)
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): Add a failure-path test for set_active_instance when a port number does not match any instance

The new tests exercise the success and HTTP rejection cases, but not the failure path for an unknown port in stdio mode. Please add a test (similar to test_port_number_not_found_raises) that calls set_active_instance in stdio mode with a port not in the fake pool and asserts it returns success: False with the expected error message, including the Available: ... list. This will better lock in the user-facing error behavior and prevent regressions.

Suggested implementation:

# ---------------------------------------------------------------------------
# set_active_instance tool: port number support
# ---------------------------------------------------------------------------

def test_set_active_instance_unknown_port_stdio(monkeypatch):
    """set_active_instance returns failure in stdio mode when the port is not in the instance pool."""
    monkeypatch.setattr(config, "transport_mode", "stdio")
    monkeypatch.setattr(config, "http_remote_hosted", False)

    from types import SimpleNamespace
    from transport.unity_instance_middleware import UnityInstanceMiddleware, set_unity_instance_middleware

    mw = UnityInstanceMiddleware()
    set_unity_instance_middleware(mw)

    # Pool has only port 6401, we'll ask for 6402.
    mw._unity_instance_pool = [
        SimpleNamespace(id="Proj@abc123", hash="abc123", port=6401),
    ]

    # The new tests for the success path should already demonstrate
    # how set_active_instance is invoked; we mirror that here.
    # If set_active_instance is a top-level helper instead of a method,
    # adjust this call to match that helper.
    result = asyncio.run(mw.set_active_instance("6402"))

    assert result["success"] is False
    # Lock in the user-visible error message including the available ports list.
    assert "No Unity instance found listening on port 6402" in result["message"]
    assert "Available: 6401" in result["message"]


def test_set_active_instance_port_stdio(monkeypatch):

You will likely need to make these small adjustments to align with the rest of your test file:

  1. Instance pool attribute name: If the middleware uses a different attribute than _unity_instance_pool to store its pool (e.g. unity_instance_pool or similar), update that line accordingly.
  2. Invocation of set_active_instance:
    • If there is a module-level function (e.g. from tools.inline_unity_instance import set_active_instance) used by the existing tests, call that instead of mw.set_active_instance.
    • Mirror exactly how test_set_active_instance_port_stdio invokes set_active_instance (async vs sync, parameter shape, etc.), and only change the port value to one that is not in the fake pool (e.g. "6402").
  3. Imports:
    • If SimpleNamespace and/or asyncio are already imported at the top of the file, you can remove the local import of SimpleNamespace and the test will still work.
  4. Error text:
    • If your existing error message wording or capitalization for the unknown-port path is slightly different (or uses a different prefix before the Available: list), update the string assertions to match the actual message, keeping the explicit check that the Available: ... portion is present.


asyncio.run(mw._inject_unity_instance(mw_ctx))

assert ctx.get_state("unity_instance") == "Proj@abc123"
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): Consider adding a test for non-string unity_instance values to exercise the str() casting path

Since _inject_unity_instance does raw_str = str(raw).strip(), it accepts non-string unity_instance values (e.g., integers). All current tests cover only string inputs. Please add a test where unity_instance is an int (e.g., 6401) and confirm it behaves like the string case, plus one for a non-numeric type to verify the expected fallback/error behavior. This will lock in the implicit str() casting behavior for future refactors.

Suggested implementation:

# ---------------------------------------------------------------------------
# Port number resolution (stdio)
# ---------------------------------------------------------------------------


def test_port_number_resolves_to_name_hash_stdio(monkeypatch):
    """Bare port number resolves to the matching Name@hash in stdio mode."""
    instances = [
        SimpleNamespace(id="Proj@abc123", hash="abc123", port=6401),
        SimpleNamespace(id="Other@def456", hash="def456", port=6402),
    ]
    mw = _make_middleware(monkeypatch, transport="stdio", pool_instances=instances)

    ctx = DummyContext()
    ctx.client_id = "client-1"
    mw_ctx = DummyMiddlewareContext(ctx, arguments={"unity_instance": "6401"})

    asyncio.run(mw._inject_unity_instance(mw_ctx))

    assert ctx.get_state("unity_instance") == "Proj@abc123"


def test_port_number_resolves_to_name_hash_stdio_int(monkeypatch):
    """Int unity_instance is accepted and resolved via str() casting in stdio mode."""
    instances = [
        SimpleNamespace(id="Proj@abc123", hash="abc123", port=6401),
        SimpleNamespace(id="Other@def456", hash="def456", port=6402),
    ]
    mw = _make_middleware(monkeypatch, transport="stdio", pool_instances=instances)

    ctx = DummyContext()
    ctx.client_id = "client-1"
    # Note: unity_instance passed as int, exercising str() casting path
    mw_ctx = DummyMiddlewareContext(ctx, arguments={"unity_instance": 6401})

    asyncio.run(mw._inject_unity_instance(mw_ctx))

    assert ctx.get_state("unity_instance") == "Proj@abc123"


def test_non_string_unity_instance_non_numeric_stdio(monkeypatch):
    """Non-numeric, non-string unity_instance exercises the str() casting fallback path."""
    instances = [
        SimpleNamespace(id="Proj@abc123", hash="abc123", port=6401),
        SimpleNamespace(id="Other@def456", hash="def456", port=6402),
    ]
    mw = _make_middleware(monkeypatch, transport="stdio", pool_instances=instances)

    class CustomUnityInstance:
        def __str__(self) -> str:
            # Use a value that will not resolve to any known instance to hit the fallback path
            return "not-a-port-or-instance-id"

    ctx = DummyContext()
    ctx.client_id = "client-1"
    mw_ctx = DummyMiddlewareContext(
        ctx,
        arguments={"unity_instance": CustomUnityInstance()},
    )

    # Depending on implementation, this may either:
    #   * Leave unity_instance unset, or
    #   * Raise a specific error when resolution fails.
    # We assume resolution failure raises a ValueError; adjust if the implementation differs.
    with pytest.raises(ValueError):
        asyncio.run(mw._inject_unity_instance(mw_ctx))

    # Ensure no unity_instance was set when resolution fails
    assert ctx.get_state("unity_instance") is None


When a tool call includes unity_instance in its arguments, the middleware:
  1. Ensure pytest is imported at the top of the file (e.g., import pytest) if it is not already present.
  2. If _inject_unity_instance uses a different exception type on resolution failure, update with pytest.raises(ValueError): to the appropriate exception class to match the actual behavior.
  3. If the real behavior on resolution failure is to not raise but to silently ignore/clear the value, remove the pytest.raises(...) context and instead assert the expected fallback behavior (e.g., ctx.get_state("unity_instance") is None and/or that no active instance changes).

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

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

107-149: _discover_instances partially duplicates the discovery logic in _maybe_autoselect_instance.

Both methods query PluginHub sessions and fall back to the stdio pool with similar exception handling. Consider extracting the shared transport-aware discovery into _discover_instances and having _maybe_autoselect_instance call it, which would reduce the duplication and ensure both paths stay in sync for future changes.

Not blocking since the two methods serve different purposes (raw discovery vs. auto-select-and-persist).

Also applies to: 225-324

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

In `@Server/src/transport/unity_instance_middleware.py` around lines 107 - 149,
The discovery logic is duplicated between _discover_instances and
_maybe_autoselect_instance; refactor by extracting the transport-aware discovery
(querying PluginHub via PluginHub.get_sessions with the same user_id/get_state
lookup and the stdio fallback using get_unity_connection_pool() and
pool.discover_all_instances(force_refresh=True), including the current exception
handling for SystemExit/KeyboardInterrupt and logger.debug) into
_discover_instances, and then have _maybe_autoselect_instance call
_discover_instances to obtain the instances and continue its
auto-select-and-persist behavior, ensuring transport selection
(config.transport_mode or "stdio") and http_remote_hosted/user_id resolution are
preserved.
🤖 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/transport/unity_instance_middleware.py`:
- Around line 151-223: The port-resolution path in _resolve_instance_value can
fail when PluginHub-provided sessions (from
PluginHub.is_configured/PluginHub.configure) are returned without a .port
attribute; update _resolve_instance_value so after calling
self._discover_instances(ctx) for a numeric value it checks whether any returned
instance has a port (e.g., using getattr(inst, "port", None)) and if none do,
call the pool-based discovery (e.g., re-invoke or call an alternate discovery
path in _discover_instances) to obtain instances with ports before attempting
the port lookup; ensure this fallback preserves the same error messages and uses
the same id/port extraction logic so port-based targeting works even when
PluginHub returns port-less sessions.

---

Nitpick comments:
In `@Server/src/transport/unity_instance_middleware.py`:
- Around line 107-149: The discovery logic is duplicated between
_discover_instances and _maybe_autoselect_instance; refactor by extracting the
transport-aware discovery (querying PluginHub via PluginHub.get_sessions with
the same user_id/get_state lookup and the stdio fallback using
get_unity_connection_pool() and pool.discover_all_instances(force_refresh=True),
including the current exception handling for SystemExit/KeyboardInterrupt and
logger.debug) into _discover_instances, and then have _maybe_autoselect_instance
call _discover_instances to obtain the instances and continue its
auto-select-and-persist behavior, ensuring transport selection
(config.transport_mode or "stdio") and http_remote_hosted/user_id resolution are
preserved.

whatevertogo added a commit to whatevertogo/unity-mcp that referenced this pull request Feb 18, 2026
…ol toggle

Merge PR CoplayDev#772 which adds per-call unity_instance routing support:
- Middleware intercepts unity_instance from tool call arguments
- Supports port number (stdio), Name@hash, and hash prefix resolution
- Per-call routing does not persist to session state

Conflicts resolved in unity_instance_middleware.py:
- Kept stdio tool toggle methods (session tracking, notifications)
- Integrated per-call routing before session fallback
- Added section comments for code organization

Co-Authored-By: dsarno <2432103+dsarno@users.noreply.github.com>
Any tool call can now include a unity_instance parameter to route that
specific call to a target Unity instance without changing the session
default and without requiring a set_active_instance call first.

The middleware pops unity_instance from tool call arguments before
Pydantic validation runs, resolves it (port number, hash prefix, or
Name@hash), and injects it into request-scoped state for that call only.

- Port numbers resolve to the matching Name@hash via status file lookup
  rather than synthetic direct:{port} IDs, so the transport layer can
  route them correctly
- HTTP mode rejects port-based targeting with a clear error
- set_active_instance now also accepts port numbers for consistency
- Multi-instance scenarios log available instances with ports when
  auto-select cannot choose
- _discover_instances() helper DRYs up transport-aware instance
  discovery previously duplicated across the codebase
- Server instructions updated to document both routing approaches
- 18 new tests covering pop behaviour, per-call vs session routing,
  port resolution, transport modes, and edge cases

Closes CoplayDev#697

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@dsarno dsarno force-pushed the feature/inline-unity-instance-routing branch from b87baaf to 0cb5a0d Compare February 18, 2026 22:03
@dsarno dsarno merged commit 4055fe5 into CoplayDev:beta Feb 18, 2026
1 of 2 checks passed
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.

🧹 Nitpick comments (3)
Server/src/services/tools/set_active_instance.py (1)

28-28: value is computed identically on both Line 28 and Line 91.

Line 91 recomputes value = (instance or "").strip() which is guaranteed to yield the same result as Line 28 (we only reach Line 91 when the digit check at Line 29 was false, meaning value is already non-digit and non-empty or empty). Consider reusing the variable from Line 28 and removing the duplicate assignment at Line 91.

Remove redundant assignment
-    value = (instance or "").strip()
-    if not value:
+    if not value:

Also applies to: 91-91

🤖 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` at line 28, Remove the
duplicate computation of value in set_active_instance (the variable computed as
(instance or "").strip()); keep the first assignment at the top (value =
(instance or "").strip()) and remove the repeated assignment at the later branch
(currently at Line 91), and update any subsequent logic to reuse that original
value (e.g., references in the digit check and the later non-digit branch) so
there is a single source of truth for value within the function.
Server/tests/integration/test_inline_unity_instance.py (2)

32-72: Test helper is well-designed; module-patching approach is sound but fragile.

The _make_middleware helper uses monkeypatch.delitem(sys.modules, ...) to force re-import of the middleware module with a patched PluginHub. This is effective for test isolation but couples tests tightly to import-time behavior — if the middleware module ever caches PluginHub at import time differently, these tests will silently break. Consider adding a brief comment at line 59 noting why the module is evicted (so the FakePluginHub is picked up on re-import).

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

In `@Server/tests/integration/test_inline_unity_instance.py` around lines 32 - 72,
Add a short inline comment in the _make_middleware helper next to the
monkeypatch.delitem(sys.modules, "transport.unity_instance_middleware",
raising=False) call explaining that the middleware module is intentionally
evicted from sys.modules so that re-importing
transport.unity_instance_middleware will pick up the patched
transport.plugin_hub.PluginHub (FakePluginHub); reference the helper name
_make_middleware and the FakePluginHub class in the comment so future readers
understand this import-time dependency and test isolation intent.

347-374: Test isolation: global middleware singleton not cleaned up after this test.

set_unity_instance_middleware(mw) at Line 354 mutates the module-level global _unity_instance_middleware, but monkeypatch only reverts attribute/item patches — it won't undo the side-effect of calling set_unity_instance_middleware. If a later test in the same process calls get_unity_instance_middleware(), it may receive this test's mw instance.

Consider patching the global via monkeypatch.setattr on the module attribute instead:

Patch via monkeypatch for automatic teardown
-    from transport.unity_instance_middleware import UnityInstanceMiddleware, set_unity_instance_middleware
+    import transport.unity_instance_middleware as uim
+    from transport.unity_instance_middleware import UnityInstanceMiddleware
     mw = UnityInstanceMiddleware()
-    set_unity_instance_middleware(mw)
+    monkeypatch.setattr(uim, "_unity_instance_middleware", mw)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Server/tests/integration/test_inline_unity_instance.py` around lines 347 -
374, The test currently calls set_unity_instance_middleware(mw) which mutates
the module-level singleton _unity_instance_middleware without automatic
teardown; instead, use monkeypatch to set the module attribute so it will be
reverted after the test: replace the direct call to
set_unity_instance_middleware with monkeypatch.setattr on the
transport.unity_instance_middleware module's _unity_instance_middleware (or on
get_unity_instance_middleware/set_unity_instance_middleware symbols) to inject
mw, ensuring the global singleton is restored by monkeypatch; reference symbols:
set_unity_instance_middleware, get_unity_instance_middleware,
_unity_instance_middleware, UnityInstanceMiddleware.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@Server/src/transport/unity_instance_middleware.py`:
- Around line 107-149: The PluginHub branch in _discover_instances builds
SimpleNamespace objects (constructed at the PluginHub.get_sessions loop) without
a .port attribute while the pool.discover_all_instances results include port
info, causing downstream port-based lookups to fail when PluginHub returns
sessions; fix by ensuring the SimpleNamespace created in the PluginHub loop
includes a .port attribute (set to the real port if available on session_info,
otherwise set to None or a sentinel) so the objects returned by
_discover_instances have a consistent shape for later port resolution and
matching with pool.discover_all_instances.

---

Nitpick comments:
In `@Server/src/services/tools/set_active_instance.py`:
- Line 28: Remove the duplicate computation of value in set_active_instance (the
variable computed as (instance or "").strip()); keep the first assignment at the
top (value = (instance or "").strip()) and remove the repeated assignment at the
later branch (currently at Line 91), and update any subsequent logic to reuse
that original value (e.g., references in the digit check and the later non-digit
branch) so there is a single source of truth for value within the function.

In `@Server/tests/integration/test_inline_unity_instance.py`:
- Around line 32-72: Add a short inline comment in the _make_middleware helper
next to the monkeypatch.delitem(sys.modules,
"transport.unity_instance_middleware", raising=False) call explaining that the
middleware module is intentionally evicted from sys.modules so that re-importing
transport.unity_instance_middleware will pick up the patched
transport.plugin_hub.PluginHub (FakePluginHub); reference the helper name
_make_middleware and the FakePluginHub class in the comment so future readers
understand this import-time dependency and test isolation intent.
- Around line 347-374: The test currently calls
set_unity_instance_middleware(mw) which mutates the module-level singleton
_unity_instance_middleware without automatic teardown; instead, use monkeypatch
to set the module attribute so it will be reverted after the test: replace the
direct call to set_unity_instance_middleware with monkeypatch.setattr on the
transport.unity_instance_middleware module's _unity_instance_middleware (or on
get_unity_instance_middleware/set_unity_instance_middleware symbols) to inject
mw, ensuring the global singleton is restored by monkeypatch; reference symbols:
set_unity_instance_middleware, get_unity_instance_middleware,
_unity_instance_middleware, UnityInstanceMiddleware.

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.

[Request] Add explicit Unity port/URL targeting for stdio mode

1 participant

Comments