Skip to content

fix: enable project-scoped custom tools in stdio mode#1111

Open
Warlander wants to merge 4 commits intoCoplayDev:betafrom
Warlander:beta
Open

fix: enable project-scoped custom tools in stdio mode#1111
Warlander wants to merge 4 commits intoCoplayDev:betafrom
Warlander:beta

Conversation

@Warlander
Copy link
Copy Markdown

@Warlander Warlander commented May 5, 2026

Description

This PR enables project-scoped custom tools ([McpForUnityTool]) to work when using the stdio transport, not just HTTP Local. Previously, stdio mode had no way to discover or register project-specific custom tools.

Type of Change

Bug fix (non-breaking change that fixes an issue)

Changes Made

ToolDiscoveryService.cs - Added AppDomain fallback scan in ToolDiscoveryService when TypeCache misses project assemblies after domain reloads
StdioBridgeHost.cs - Heartbeat JSON now includes project_scoped_tools flag so the server knows when a Unity instance wants project-scoped tooling
models.py / port_discovery.py / main.py — UnityInstanceInfo gains project_scoped_tools; PortDiscovery parses it from status JSON; server auto-enables project-scoped tools on startup when a discovered instance requests it
custom_tool_service.py - CustomToolService now registers global custom tools even when project-scoped tools are enabled
v8_NEW_NETWORKING_SETUP.md - Updated migration notes to reflect that custom tools now work in both HTTP and stdio transports

Testing/Screenshots/Recordings

  • Custom tools annotated with [McpForUnityTool] appear in the MCP tool list when using stdio transport
  • No regression in HTTP Local transport custom tool discovery

Documentation Updates

  • I have added/removed/modified tools or resources
  • If yes, I have updated all documentation files using:
    • The LLM prompt at tools/UPDATE_DOCS_PROMPT.md (recommended)
    • Manual updates following the guide at tools/UPDATE_DOCS.md

(none of above apply)

Related Issues

Additional Notes

If you would like to see any changes to this PR, please let me know and I will get to it! :) My personal motivation behind this PR is, Kimi Code plays poorly with HTTP so I'm forced to use stdio with it, and having support for custom MCP's would be nice to have. Changes made by Kimi-k2.6 and human reviewed (and tested in actual project) by me to make sure they make sense and don't introduce possible vulnerabilities or other issues.

Summary by CodeRabbit

  • New Features

    • Project-scoped tools can now be dynamically enabled from Unity instances based on heartbeat signals.
    • Custom tools now work with both HTTP and stdio transports.
    • Added per-project port discovery support for multi-project environments.
  • Improvements

    • Enhanced tool discovery resilience with improved error handling and fallback mechanisms.
    • Global tools are now registered alongside project-scoped tools for broader compatibility.
  • Documentation

    • Updated documentation to reflect custom tool transport support across all transport types.

Warlander added 4 commits May 5, 2026 22:06
- ToolDiscoveryService: add AppDomain fallback scan for [McpForUnityTool] types
- StdioBridgeHost: include project_scoped_tools flag in heartbeat JSON
- McpToolsSection: update tooltip and default to reflect stdio support
- models.py: add project_scoped_tools field to UnityInstanceInfo
- port_discovery.py: read project_scoped_tools from status JSON
- main.py: enable project-scoped tools when Unity instance requests it
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

📝 Walkthrough

Walkthrough

This PR enhances tool discovery resilience and introduces a project-scoped tools feature. It adds TypeCache-plus-AppDomain reflection fallback to editor tool discovery, propagates a new project_scoped_tools flag from editor to server via heartbeat and port discovery, enables dynamic enablement of project-scoped tools at server startup, and removes a guard that previously skipped global tool registration when project-scoped tools were active.

Changes

Tool Discovery & Project-Scoped Tools

Layer / File(s) Summary
Data Shape
Server/src/models/models.py
UnityInstanceInfo gains a new project_scoped_tools: bool = False field and updated to_dict() to serialize it.
Editor → Server Communication
MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs, Server/src/transport/legacy/port_discovery.py
Heartbeat payload now includes project_scoped_tools read from EditorPrefs; port discovery reads project_scoped_tools and unity_version from status data and passes them to UnityInstanceInfo.
Tool Discovery Robustness
MCPForUnity/Editor/Services/ToolDiscoveryService.cs
Tool discovery now performs a primary TypeCache scan followed by AppDomain reflection fallback for non-dynamic assemblies, with per-assembly error handling and deduplication before registration.
Server Initialization & Tool Registration
Server/src/main.py, Server/src/services/custom_tool_service.py
Server startup now reads explicit CLI/env overrides for project-scoped tools and dynamically enables them if any instance requests it; custom tool service no longer skips global tool registration when project-scoped tools are active, instead registering unconditionally.
UI & Documentation
MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs, docs/migrations/v8_NEW_NETWORKING_SETUP.md
Project-scoped tools toggle tooltip updated to mention both HTTP Local and stdio transports; migration docs note that custom tools now work in both HTTP and stdio transports.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

  • CoplayDev/unity-mcp#837: Tool discovery was reworked to add an AppDomain reflection fallback merged with TypeCache results and per-type error handling, which directly addresses the stdio-mode custom-tool discovery failure described.

Possibly related PRs

  • CoplayDev/unity-mcp#596: Both PRs implement the same "project_scoped_tools" feature and touch the same code paths (EditorPrefKeys, server main initialization, CustomToolService/tool registration behavior, and editor UI/transport wiring).
  • CoplayDev/unity-mcp#517: Both PRs modify Server/src/services/custom_tool_service.py and related Unity instance project-ID handling, changing how tools are registered for instances.
  • CoplayDev/unity-mcp#636: Both PRs implement and wire up custom-tool discovery/execution flows and touch instance/tool metadata (e.g., project_scoped_tools/UnityInstanceInfo).

Poem

🐰 Two scans now dance where once was one,
TypeCache swift, and AppDomain's run.
Project-scoped tools leap through both ports,
While global tools join all the cohorts!
Discovery blooms resilient and bright.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% 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 accurately summarizes the main objective: enabling project-scoped custom tools in stdio mode, which is the primary feature change across all modified files.
Description check ✅ Passed The description covers all required sections: clear problem statement, type of change (bug fix), detailed changes across affected files, testing checklist, and related context. All critical information is present.
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.

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

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

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

@Warlander Warlander changed the title Enable project-scoped custom tools in stdio mode fix: enable project-scoped custom tools in stdio mode May 5, 2026
Copy link
Copy Markdown
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.

Caution

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

⚠️ Outside diff range comments (1)
MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs (1)

1050-1066: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Heartbeat default contradicts UI toggle default for the same EditorPref.

StdioBridgeHost.WriteHeartbeat reads EditorPrefKeys.ProjectScopedToolsLocalHttp with default true, but McpToolsSection.RegisterCallbacks (Line 76–79) reads the same key with default false. On a fresh install (key absent):

  • The UI toggle visibly shows OFF.
  • The heartbeat reports project_scoped_tools=true.
  • Server/src/main.py (Line 896–909) sees the flag and auto-enables project-scoped mode without the user opting in.

Result: the server runs in project-scoped mode while the editor UI claims it's disabled, which is confusing and breaks the "Explicit CLI/env overrides always win; otherwise honor what Unity reports" contract.

Pick one default and use it in both places (and ideally read it through a shared helper).

🛠️ One-line alignment if "off by default" is the intended behavior
                 bool projectScopedTools = EditorPrefs.GetBool(
                     EditorPrefKeys.ProjectScopedToolsLocalHttp,
-                    true  // default to true so stdio behaves like HTTP Local by default
+                    false // must match McpToolsSection toggle default so UI and heartbeat agree
                 );

If "on by default" is preferred instead, flip falsetrue in McpToolsSection.cs Line 78 to match.

🤖 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 `@MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs` around
lines 1050 - 1066, StdioBridgeHost.WriteHeartbeat is reading
EditorPrefKeys.ProjectScopedToolsLocalHttp with default true while
McpToolsSection.RegisterCallbacks uses default false, causing inconsistent
behavior; fix by centralizing the preference read (e.g., add a helper like
GetProjectScopedToolsDefault or a static method on EditorPrefKeys) and use that
helper from both StdioBridgeHost.WriteHeartbeat and
McpToolsSection.RegisterCallbacks, or change the literal default in
StdioBridgeHost.WriteHeartbeat from true to false so both use the same default;
reference EditorPrefKeys.ProjectScopedToolsLocalHttp,
StdioBridgeHost.WriteHeartbeat, and McpToolsSection.RegisterCallbacks when
making the change.
🧹 Nitpick comments (2)
MCPForUnity/Editor/Services/ToolDiscoveryService.cs (1)

26-49: 💤 Low value

Two-phase scan with dedupe looks correct.

TypeCache results are unioned with an AppDomain reflection pass and deduped by Type identity, so post-domain-reload misses on project assemblies are now covered without double-registering. The per-assembly try/catch around GetTypes() is the right pattern for handling ReflectionTypeLoadException from third-party assemblies.

Minor: new Type[0] can be Array.Empty<Type>() to avoid an allocation per failing assembly.

🤖 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 `@MCPForUnity/Editor/Services/ToolDiscoveryService.cs` around lines 26 - 49,
Replace the allocation of an empty array inside the AppDomain reflection
fallback with a shared empty array to avoid per-assembly allocations: in the
lambda used after AppDomain.CurrentDomain.GetAssemblies() where you catch
exceptions from a.GetTypes() (in the code that calls
TypeCache.GetTypesWithAttribute<McpForUnityToolAttribute>() and then
SelectMany(...)), return Array.Empty<Type>() instead of new Type[0] in the catch
block.
Server/src/main.py (1)

893-912: 💤 Low value

Auto-detection works, but consider two follow-ups.

  1. Duplicate discovery work at startup. pool.discover_all_instances() here (Line 900) is called again inside server_lifespan (Line 200). Each call walks status files and TCP-probes every discovered port (_try_probe_unity_mcp with 0.3s timeout each). For a typical single-instance setup this is fine, but you could cache the result on the pool or pass it through the lifespan via the yielded dict to avoid the second probe pass.

  2. One-shot decision. project_scoped_tools is fixed for the life of the server based on what Unity reported at startup. If the user toggles the EditorPref afterwards, the server will not pick it up until restart. If that's by design, a brief log line documenting it (or a docstring note) would help future debugging.

Static-analysis BLE001 on Line 910 is acceptable here — the broad except is paired with logger.debug(..., exc_info=True) and a safe fallback to the explicit value, so no action needed.

🤖 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 893 - 912, The startup code calls
get_unity_connection_pool().discover_all_instances() to set
project_scoped_tools, which causes duplicate expensive probes because
server_lifespan also calls pool.discover_all_instances(); to fix, either cache
the discovery result on the pool object (e.g., set
pool._cached_discovered_instances after the first call and use it in subsequent
discover_all_instances calls) or pass the discovered instances through the
lifespan handshake (include the instances list in the dict yielded by
server_lifespan) so the second probe is avoided; also add a concise log or
docstring near project_scoped_tools / the startup block explaining this is a
one-shot decision (toggled Unity EditorPref after startup won’t change the
server’s behavior) so future debuggers aren’t surprised.
🤖 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.

Outside diff comments:
In `@MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs`:
- Around line 1050-1066: StdioBridgeHost.WriteHeartbeat is reading
EditorPrefKeys.ProjectScopedToolsLocalHttp with default true while
McpToolsSection.RegisterCallbacks uses default false, causing inconsistent
behavior; fix by centralizing the preference read (e.g., add a helper like
GetProjectScopedToolsDefault or a static method on EditorPrefKeys) and use that
helper from both StdioBridgeHost.WriteHeartbeat and
McpToolsSection.RegisterCallbacks, or change the literal default in
StdioBridgeHost.WriteHeartbeat from true to false so both use the same default;
reference EditorPrefKeys.ProjectScopedToolsLocalHttp,
StdioBridgeHost.WriteHeartbeat, and McpToolsSection.RegisterCallbacks when
making the change.

---

Nitpick comments:
In `@MCPForUnity/Editor/Services/ToolDiscoveryService.cs`:
- Around line 26-49: Replace the allocation of an empty array inside the
AppDomain reflection fallback with a shared empty array to avoid per-assembly
allocations: in the lambda used after AppDomain.CurrentDomain.GetAssemblies()
where you catch exceptions from a.GetTypes() (in the code that calls
TypeCache.GetTypesWithAttribute<McpForUnityToolAttribute>() and then
SelectMany(...)), return Array.Empty<Type>() instead of new Type[0] in the catch
block.

In `@Server/src/main.py`:
- Around line 893-912: The startup code calls
get_unity_connection_pool().discover_all_instances() to set
project_scoped_tools, which causes duplicate expensive probes because
server_lifespan also calls pool.discover_all_instances(); to fix, either cache
the discovery result on the pool object (e.g., set
pool._cached_discovered_instances after the first call and use it in subsequent
discover_all_instances calls) or pass the discovered instances through the
lifespan handshake (include the instances list in the dict yielded by
server_lifespan) so the second probe is avoided; also add a concise log or
docstring near project_scoped_tools / the startup block explaining this is a
one-shot decision (toggled Unity EditorPref after startup won’t change the
server’s behavior) so future debuggers aren’t surprised.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 29975faa-4cf0-437c-beba-ccfe05006e2d

📥 Commits

Reviewing files that changed from the base of the PR and between a2a5edf and bb17565.

📒 Files selected for processing (8)
  • MCPForUnity/Editor/Services/ToolDiscoveryService.cs
  • MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs
  • MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs
  • Server/src/main.py
  • Server/src/models/models.py
  • Server/src/services/custom_tool_service.py
  • Server/src/transport/legacy/port_discovery.py
  • docs/migrations/v8_NEW_NETWORKING_SETUP.md

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