Skip to content

Add per-call Unity instance targeting#1277

Open
Qommon wants to merge 6 commits into
CoplayDev:betafrom
Qommon:feat/per-call-instance-targeting
Open

Add per-call Unity instance targeting#1277
Qommon wants to merge 6 commits into
CoplayDev:betafrom
Qommon:feat/per-call-instance-targeting

Conversation

@Qommon

@Qommon Qommon commented Jul 19, 2026

Copy link
Copy Markdown

Description

This PR adds per-call Unity Editor instance targeting in the Python transport layer. The normal session-scoped active instance remains the recommended workflow; an explicit selector is an optional override for one call. We do need this feature for concurrent agent workflows in our practical development.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactoring (no functional changes)
  • Test update

What

  • Add request-local per-call Unity instance routing for HTTP /api/command, HTTP /api/custom-tools?instance=..., targetable MCP Tools, and Unity-backed MCP Resources.
  • Resolve explicit targets before active/default routing, with the priority per-call target > session active instance > existing beta auto/default behavior.
  • Keep active-instance state session-scoped and keep per-call effective routing request-scoped, so explicit targets do not persist or cross-contaminate concurrent calls.
  • Consume routing metadata in the Python middleware before FastMCP validation or Resource dispatch. Unity command params remain unchanged.
  • Preserve the existing selector forms and no-target behavior, including Name@hash, unique hash prefixes, stdio port selectors, HTTP numeric hashes, active selection, single-instance auto-selection, and the existing REST fallback behavior.
  • Keep batch_execute inner-command routing out of the Unity payload.

No Unity C# production code, Command model, WebSocket execute envelope, or HandleCommand(JObject @params) signature was changed. This change adds routing logic on the Python side only.

Why

In practical development, multiple Agents frequently work concurrently against multiple Unity Editors and/or separate repository copies. A session-wide active instance is useful for normal work, but it cannot safely express overlapping calls that must go to different Editors. A request-local explicit target provides atomic per-call routing without switching or mutating the session default.

Compatibility / Package Source

  • Unity version tested: Unity 6000.0.75f1.
  • Package source used: local file: reference to the current working tree's MCPForUnity package.
  • Two temporary projects (Alpha and Beta) imported and compiled the package successfully.
  • No Unity C# production files are changed by this PR.

Testing / Screenshots / Recordings

  • Python tests: Server\.venv\Scripts\python.exe -m pytest tests/ -q (1351 passed, 3 skipped, 3 warnings).
  • Unity EditMode tests (not run; this change does not modify Unity C# production code).
  • Unity PlayMode tests (not run; this change does not modify Unity C# production code).
  • Package import/compile check.
  • Real local two-Editor HTTP/WebSocket E2E.

The E2E used one local Python Server, two real Unity Editor processes, and one FastMCP Client with one MCP session. It verified:

  • HTTP /api/command and /api/custom-tools?instance=... targeting A/B and overlapping HTTP calls.
  • MCP Tool and Unity-backed Resource targeting A/B concurrently in the same MCP session.
  • Explicit Beta calls leaving an active Alpha fallback unchanged.
  • Invalid selectors failing without falling back to another instance.
  • Project-root and active-scene identities returned by the two Editors remaining distinguishable.

The repository's documented uv run --frozen pytest invocation was also attempted but could not start because of the local uv trampoline error (uv trampoline failed to canonicalize script path); the full suite passed through the repository virtualenv invocation above.

Documentation Updates

  • I have added/removed/modified tools or resources.
  • The LLM prompt at tools/UPDATE_DOCS_PROMPT.md was not used.
  • The related multi-instance and remote-auth documentation changes were manually reviewed.

Related Issues

None.

Additional Notes

This is one implementation generated by GPT 5.6 Luna Max. It works well in the tested scenario, and I would appreciate maintainers' review, feedback, and suggestions.

Summary by CodeRabbit

  • New Features

    • Added per-call Unity instance targeting for tools, resources, and HTTP commands.
    • Added clearer instance selection using names, hashes, and supported transport-specific selectors.
    • Targetable tools now advertise the unity_instance routing option.
    • Added request-scoped routing to support concurrent calls without instance-selection leakage.
  • Bug Fixes

    • Explicitly targeted instances no longer silently fall back to a default instance after disconnects.
    • Improved validation and error responses for invalid or ambiguous instance selectors.
  • Documentation

    • Updated multi-instance, remote authentication, and tool reference documentation.

Qommon added 2 commits July 20, 2026 04:30
Add per-call Unity instance selectors for tools, resources, and local HTTP commands while keeping active-instance state session-scoped and effective targets request-local.

Preserve stdio discovery and retry behavior, and update coverage and multi-instance routing documentation.
Keep numeric HTTP project names ahead of hash and port shorthand, and preserve mcp_for_unity_tool's legacy group positional argument.
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 89636741-eed6-4a0f-9cf3-c18707dc3095

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

The PR adds canonical Unity instance resolution across MCP middleware and HTTP endpoints, request-scoped per-call routing, targetable tool/resource metadata, targeted resource templates, and explicit-target stdio retry behavior. Tests and documentation cover routing precedence, concurrency, transport differences, and recovery.

Changes

Unity instance targeting

Layer / File(s) Summary
Targetable tool and resource registration
Server/src/services/registry/*, Server/src/services/resources/*, Server/src/services/tools/execute_custom_tool.py, Server/src/services/custom_tool_service.py, Server/tests/test_tool_registry_metadata.py
Tool and resource registries record targetability, apply routing tags, expose targeted resource templates, and validate targetability metadata.
Request-scoped instance routing
Server/src/transport/unity_instance_middleware.py, Server/tests/integration/*, website/docs/architecture/remote-auth.md, website/docs/guides/multi-instance.md
Middleware resolves explicit selectors, consumes tool/resource routing metadata, injects request-local state, and annotates targetable tools with the optional unity_instance schema.
HTTP endpoint target resolution
Server/src/main.py, Server/tests/test_instance_targeting_http.py
HTTP command and custom-tool routes use canonical resolver output and return structured resolver errors.
Explicit stdio target recovery
Server/src/transport/legacy/unity_connection.py, Server/tests/test_transport_characterization.py
Explicit stdio targets are rediscovered during retries without default fallback when unavailable, while instance scans are serialized under the pool lock.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant HTTPClient
  participant MainAPI
  participant UnityInstanceResolver
  participant UnitySession
  HTTPClient->>MainAPI: POST /api/command with unity_instance
  MainAPI->>UnityInstanceResolver: resolve_instance_identifier
  UnityInstanceResolver-->>MainAPI: canonical instance id
  MainAPI->>UnitySession: execute command for resolved session
  UnitySession-->>MainAPI: command response
  MainAPI-->>HTTPClient: JSON response or structured targeting error
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.83% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding per-call Unity instance targeting.
Description check ✅ Passed The description covers the required sections and includes the core change, testing, compatibility, docs, and notes, though it uses a custom 'What' section.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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.

@Qommon
Qommon marked this pull request as ready for review July 19, 2026 21:44

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 (6)
Server/tests/integration/test_instance_autoselect.py (1)

110-140: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider using MagicMock instead of manually stubbing types.ModuleType.

Replacing whole modules in sys.modules with an empty types.ModuleType is brittle. If UnityInstanceMiddleware eventually imports other, unrelated symbols from transport.plugin_hub or transport.legacy.unity_connection, the test will crash with an AttributeError.

Using unittest.mock.MagicMock for the module stubs automatically provisions any requested attributes, preventing unexpected import failures. It also provides built-in assertions, eliminating the need to manually capture arguments with local lists like refresh_values. Consider applying this cleaner pattern here and in similar tests below.

♻️ Proposed refactor
-    plugin_hub = types.ModuleType("transport.plugin_hub")
-
-    class PluginHub:
-        `@classmethod`
-        def is_configured(cls) -> bool:
-            return False
-
-    plugin_hub.PluginHub = PluginHub
-    monkeypatch.setitem(sys.modules, "transport.plugin_hub", plugin_hub)
+    from unittest.mock import MagicMock
+    plugin_hub_mock = MagicMock()
+    plugin_hub_mock.PluginHub.is_configured.return_value = False
+    monkeypatch.setitem(sys.modules, "transport.plugin_hub", plugin_hub_mock)
     monkeypatch.delitem(sys.modules, "transport.unity_instance_middleware", raising=False)
 
     from transport.unity_instance_middleware import UnityInstanceMiddleware
 
     monkeypatch.setattr(config, "transport_mode", "stdio")
     middleware = UnityInstanceMiddleware()
     ctx = DummyContext()
-    refresh_values = []
-
-    class PoolStub:
-        def discover_all_instances(self, force_refresh=False):
-            refresh_values.append(force_refresh)
-            return [SimpleNamespace(id="UnityMCPTests@cc8756d4")]
-
-    unity_connection = types.ModuleType("transport.legacy.unity_connection")
-    unity_connection.get_unity_connection_pool = lambda: PoolStub()
-    monkeypatch.setitem(sys.modules, "transport.legacy.unity_connection", unity_connection)
+
+    mock_pool = MagicMock()
+    mock_pool.discover_all_instances.return_value = [SimpleNamespace(id="UnityMCPTests@cc8756d4")]
+    unity_connection_mock = MagicMock()
+    unity_connection_mock.get_unity_connection_pool.return_value = mock_pool
+    monkeypatch.setitem(sys.modules, "transport.legacy.unity_connection", unity_connection_mock)
 
     instances = await middleware._discover_instances(ctx)
 
     assert [instance.id for instance in instances] == ["UnityMCPTests@cc8756d4"]
-    assert refresh_values == [True]
+    mock_pool.discover_all_instances.assert_called_once_with(force_refresh=True)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Server/tests/integration/test_instance_autoselect.py` around lines 110 - 140,
Update the module stubs in the instance auto-selection tests to use
unittest.mock.MagicMock instead of manually created types.ModuleType instances.
Replace local argument-capture lists such as refresh_values with MagicMock call
assertions, and apply the same pattern to the similar tests below while
preserving their existing behavior.
Server/src/main.py (1)

482-491: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove dead code.

Since _resolve_http_instance_target now raises an InstanceTargetError that is caught and returned immediately above, session_id will always be populated if a valid unity_instance was provided. This fallback check is now dead code and can be safely removed.

♻️ Proposed refactor
-                # If a specific unity_instance was requested but not found, return an error
-                # (Check done here so execute_custom_tool can also validate the instance)
-                if unity_instance and not session_id:
-                    return JSONResponse(
-                        {
-                            "success": False,
-                            "error": f"Unity instance '{unity_instance}' not found",
-                        },
-                        status_code=404,
-                    )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Server/src/main.py` around lines 482 - 491, Remove the fallback `if
unity_instance and not session_id` block from the surrounding request handler,
including its 404 `JSONResponse`; `_resolve_http_instance_target` and its
existing `InstanceTargetError` handling already cover invalid Unity instances,
while valid targets populate `session_id`.
Server/tests/test_instance_targeting_http.py (1)

57-57: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use a raw string for regex patterns.

The pattern passed to match= contains regex metacharacters (.*). It's recommended to use a raw string to avoid unintended escape sequence behaviors and to satisfy linters like Ruff (RUF043).

♻️ Proposed refactor
-    with pytest.raises(InstanceTargetError, match="Project name 'Project'.*multiple"):
+    with pytest.raises(InstanceTargetError, match=r"Project name 'Project'.*multiple"):
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Server/tests/test_instance_targeting_http.py` at line 57, Update the
pytest.raises call’s match pattern in the relevant instance-targeting test to
use a raw string literal, preserving the existing regex and expected error
message.

Source: Linters/SAST tools

Server/tests/test_transport_characterization.py (1)

1094-1094: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use a raw string for the match= regex. pytest.raises(match=...) runs re.search, so .* is intentional regex; a raw string documents that intent and clears Ruff RUF043 at both sites.

  • Server/tests/test_transport_characterization.py#L1094-L1094: change to match=r"target-hash.*no longer available".
  • Server/tests/test_transport_characterization.py#L1139-L1139: change to match=r"target-hash.*no longer available".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Server/tests/test_transport_characterization.py` at line 1094, Update both
pytest.raises match expressions in
Server/tests/test_transport_characterization.py at lines 1094-1094 and 1139-1139
to use raw regex strings for the existing target-hash pattern, preserving the
current matching behavior and clearing Ruff RUF043.

Source: Linters/SAST tools

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

75-80: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use .pop() instead of if key in dict: del dict[key].

Ruff (RUF051) flags this pattern; tool_kwargs.pop("unity_targetable", None) is more idiomatic.

♻️ Proposed fix
         tool_kwargs = dict(kwargs)  # Create a copy to avoid side effects
-        if "unity_target" in tool_kwargs:
-            del tool_kwargs["unity_target"]
-        if "unity_targetable" in tool_kwargs:
-            del tool_kwargs["unity_targetable"]
-        if "group" in tool_kwargs:
-            del tool_kwargs["group"]
+        tool_kwargs.pop("unity_target", None)
+        tool_kwargs.pop("unity_targetable", None)
+        tool_kwargs.pop("group", None)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Server/src/services/registry/tool_registry.py` around lines 75 - 80, In the
tool_kwargs cleanup block, replace the membership checks and deletions for
unity_target, unity_targetable, and group with dict.pop calls using a None
default, preserving the behavior when keys are absent.

Source: Linters/SAST tools

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

536-537: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace setattr with a direct attribute assignment (Ruff B010).

The attribute name is a constant, so setattr offers nothing over assignment.

♻️ Proposed tweak
             clean_message = copy(message)
-            setattr(clean_message, "uri", clean_uri)
+            clean_message.uri = clean_uri
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Server/src/transport/unity_instance_middleware.py` around lines 536 - 537, In
the affected middleware code, replace the setattr call using the constant
attribute name with direct attribute assignment on the target object, preserving
the assigned value and surrounding behavior.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Server/src/services/tools/execute_custom_tool.py`:
- Around line 19-20: Update the MCP tool declaration in execute_custom_tool to
replace group=None with an allowed registered group, selecting the value
appropriate for this tool such as scripting_ext or core, while preserving
unity_targetable=True.

In `@Server/src/transport/legacy/unity_connection.py`:
- Around line 584-599: Update the instance-discovery method to avoid holding
_pool_lock during PortDiscovery.discover_all_unity_instances(). Add and
initialize a dedicated _scan_lock, use _pool_lock only for cache reads and
updates, and re-check the cache after acquiring _scan_lock before performing a
forced scan.
- Around line 547-549: Update the `except Exception as e` block in the
connection or rediscovery flow to raise `target_unavailable_error` with explicit
exception chaining from `e`, preserving the original traceback while keeping the
existing error behavior unchanged.

---

Nitpick comments:
In `@Server/src/main.py`:
- Around line 482-491: Remove the fallback `if unity_instance and not
session_id` block from the surrounding request handler, including its 404
`JSONResponse`; `_resolve_http_instance_target` and its existing
`InstanceTargetError` handling already cover invalid Unity instances, while
valid targets populate `session_id`.

In `@Server/src/services/registry/tool_registry.py`:
- Around line 75-80: In the tool_kwargs cleanup block, replace the membership
checks and deletions for unity_target, unity_targetable, and group with dict.pop
calls using a None default, preserving the behavior when keys are absent.

In `@Server/src/transport/unity_instance_middleware.py`:
- Around line 536-537: In the affected middleware code, replace the setattr call
using the constant attribute name with direct attribute assignment on the target
object, preserving the assigned value and surrounding behavior.

In `@Server/tests/integration/test_instance_autoselect.py`:
- Around line 110-140: Update the module stubs in the instance auto-selection
tests to use unittest.mock.MagicMock instead of manually created
types.ModuleType instances. Replace local argument-capture lists such as
refresh_values with MagicMock call assertions, and apply the same pattern to the
similar tests below while preserving their existing behavior.

In `@Server/tests/test_instance_targeting_http.py`:
- Line 57: Update the pytest.raises call’s match pattern in the relevant
instance-targeting test to use a raw string literal, preserving the existing
regex and expected error message.

In `@Server/tests/test_transport_characterization.py`:
- Line 1094: Update both pytest.raises match expressions in
Server/tests/test_transport_characterization.py at lines 1094-1094 and 1139-1139
to use raw regex strings for the existing target-hash pattern, preserving the
current matching behavior and clearing Ruff RUF043.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 39fbde90-c6d0-470c-9078-ca06fc62462a

📥 Commits

Reviewing files that changed from the base of the PR and between bd72241 and 96a3535.

📒 Files selected for processing (23)
  • Server/src/main.py
  • Server/src/services/custom_tool_service.py
  • Server/src/services/registry/__init__.py
  • Server/src/services/registry/resource_registry.py
  • Server/src/services/registry/tool_registry.py
  • Server/src/services/resources/__init__.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/transport/legacy/unity_connection.py
  • Server/src/transport/unity_instance_middleware.py
  • Server/tests/integration/test_helpers.py
  • Server/tests/integration/test_inline_unity_instance.py
  • Server/tests/integration/test_instance_autoselect.py
  • Server/tests/test_instance_targeting_http.py
  • Server/tests/test_tool_registry_metadata.py
  • Server/tests/test_transport_characterization.py
  • website/docs/architecture/remote-auth.md
  • website/docs/guides/multi-instance.md
  • website/docs/reference/tools/core/manage_material.md
  • website/docs/reference/tools/vfx/manage_texture.md

Comment thread Server/src/services/tools/execute_custom_tool.py Outdated
Comment thread Server/src/transport/legacy/unity_connection.py
Comment thread Server/src/transport/legacy/unity_connection.py Outdated
@Qommon

Qommon commented Jul 19, 2026

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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