Skip to content

Commit 0bb4478

Browse files
authored
Merge pull request #1111 from Warlander/beta
fix: enable project-scoped custom tools in stdio mode
2 parents b98193d + f5de4ed commit 0bb4478

8 files changed

Lines changed: 64 additions & 9 deletions

File tree

MCPForUnity/Editor/Services/ToolDiscoveryService.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,30 @@ public List<ToolMetadata> DiscoverAllTools()
2323

2424
_cachedTools = new Dictionary<string, ToolMetadata>();
2525

26+
// Primary scan via TypeCache (fast, but can miss project assemblies in some domain-reload states)
2627
var toolTypes = TypeCache.GetTypesWithAttribute<McpForUnityToolAttribute>();
27-
foreach (var type in toolTypes)
28+
29+
// Fallback scan via AppDomain (slower but exhaustive; mirrors CommandRegistry behaviour)
30+
var appDomainTypes = AppDomain.CurrentDomain.GetAssemblies()
31+
.Where(a => !a.IsDynamic)
32+
.SelectMany(a =>
33+
{
34+
try { return a.GetTypes(); }
35+
catch (Exception ex)
36+
{
37+
McpLog.Warn($"Failed to reflect types from assembly {a.FullName}: {ex.Message}");
38+
return new Type[0];
39+
}
40+
})
41+
.Where(t => t.GetCustomAttribute<McpForUnityToolAttribute>() != null);
42+
43+
// Merge both scans, deduplicating by type
44+
var allToolTypes = toolTypes
45+
.Concat(appDomainTypes)
46+
.Distinct()
47+
.ToList();
48+
49+
foreach (var type in allToolTypes)
2850
{
2951
McpForUnityToolAttribute toolAttr;
3052
try

MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1047,6 +1047,11 @@ public static void WriteHeartbeat(bool reloading, string reason = null)
10471047
}
10481048
catch { }
10491049

1050+
bool projectScopedTools = EditorPrefs.GetBool(
1051+
EditorPrefKeys.ProjectScopedToolsLocalHttp,
1052+
false // must match McpToolsSection toggle default so UI and heartbeat agree
1053+
);
1054+
10501055
var payload = new
10511056
{
10521057
unity_port = currentUnityPort,
@@ -1056,7 +1061,8 @@ public static void WriteHeartbeat(bool reloading, string reason = null)
10561061
project_path = Application.dataPath,
10571062
project_name = projectName,
10581063
unity_version = Application.unityVersion,
1059-
last_heartbeat = DateTime.UtcNow.ToString("O")
1064+
last_heartbeat = DateTime.UtcNow.ToString("O"),
1065+
project_scoped_tools = projectScopedTools
10601066
};
10611067
File.WriteAllText(filePath, JsonConvert.SerializeObject(payload), new System.Text.UTF8Encoding(false));
10621068
}

MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ private void RegisterCallbacks()
7777
EditorPrefKeys.ProjectScopedToolsLocalHttp,
7878
false
7979
);
80-
projectScopedToolsToggle.tooltip = "When enabled, register project-scoped tools with HTTP Local transport. Allows per-project tool customization.";
80+
projectScopedToolsToggle.tooltip = "When enabled, register project-scoped tools with HTTP Local and stdio transports. Allows per-project tool customization.";
8181
projectScopedToolsToggle.RegisterValueChangedCallback(evt =>
8282
{
8383
EditorPrefs.SetBool(EditorPrefKeys.ProjectScopedToolsLocalHttp, evt.newValue);

Server/src/main.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -884,10 +884,32 @@ def main():
884884
if args.http_port:
885885
logger.info(f"HTTP port override: {http_port}")
886886

887-
project_scoped_tools = (
887+
# Explicit CLI/env overrides always win
888+
project_scoped_tools_explicit = (
888889
bool(args.project_scoped_tools)
889890
or os.environ.get("UNITY_MCP_PROJECT_SCOPED_TOOLS", "").lower() in ("true", "1", "yes", "on")
890891
)
892+
893+
# If not explicitly set, check Unity status files for the default instance.
894+
# In stdio mode there is typically only one instance, so "first match wins" is fine.
895+
project_scoped_tools = project_scoped_tools_explicit
896+
if not project_scoped_tools_explicit:
897+
try:
898+
from transport.legacy.unity_connection import get_unity_connection_pool
899+
pool = get_unity_connection_pool()
900+
instances = pool.discover_all_instances()
901+
# If ANY discovered instance requests project-scoped tools, enable them
902+
for inst in instances:
903+
if getattr(inst, "project_scoped_tools", False):
904+
project_scoped_tools = True
905+
logger.info(
906+
"Enabling project-scoped tools because Unity instance %s requested it",
907+
inst.id,
908+
)
909+
break
910+
except Exception:
911+
logger.debug("Could not discover Unity instances for project-scoped tool default", exc_info=True)
912+
891913
mcp = create_mcp_server(project_scoped_tools)
892914

893915
# Determine transport mode

Server/src/models/models.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class UnityInstanceInfo(BaseModel):
4242
status: str # "running", "reloading", "offline"
4343
last_heartbeat: datetime | None = None
4444
unity_version: str | None = None
45+
project_scoped_tools: bool = False
4546

4647
def to_dict(self) -> dict[str, Any]:
4748
"""Convert to dictionary for JSON serialization"""
@@ -53,5 +54,6 @@ def to_dict(self) -> dict[str, Any]:
5354
"port": self.port,
5455
"status": self.status,
5556
"last_heartbeat": self.last_heartbeat.isoformat() if self.last_heartbeat else None,
56-
"unity_version": self.unity_version
57+
"unity_version": self.unity_version,
58+
"project_scoped_tools": self.project_scoped_tools,
5759
}

Server/src/services/custom_tool_service.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -327,8 +327,9 @@ def _register_project_tools(
327327
return registered, replaced
328328

329329
def register_global_tools(self, tools: list[ToolDefinitionModel]) -> None:
330-
if self._project_scoped_tools:
331-
return
330+
# Global custom tools are always registered, even when project-scoped tools
331+
# are enabled. Project-scoped tools can override globals by name, but
332+
# disabling globals entirely would break shared tooling that projects expect.
332333
builtin_names = self._get_builtin_tool_names()
333334
for tool in tools:
334335
if tool.name in builtin_names:

Server/src/transport/legacy/port_discovery.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,8 @@ def discover_all_unity_instances() -> list[UnityInstanceInfo]:
309309
status="reloading" if is_reloading else "running",
310310
last_heartbeat=last_heartbeat,
311311
# May not be available in current version
312-
unity_version=data.get('unity_version')
312+
unity_version=data.get('unity_version'),
313+
project_scoped_tools=data.get('project_scoped_tools', False),
313314
)
314315

315316
instances_by_port[port] = (instance, freshness)

docs/migrations/v8_NEW_NETWORKING_SETUP.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,4 +257,5 @@ This was a big change, and it touches all the repo. So a lot of inefficiencies a
257257
- ~~Think about a structure of the MCP server some more. The `tools`, `resources` and `registry` folders make sense, but everything else just forms part of the high level repo. It's growing, so some thought about how we create modules will help with scalability.~~
258258
- This was done, Server folder is much more hierarchical and structured.
259259
- The way we register tools is a good platform for all tools to be defined by C#. Having all tools in the plugin makes it easier for us to maintain, the community to contribute, and users to modify this project to suit their needs. If all tools are registered from the plugin, we can allow users to select the tools they want to use, giving them even more control of their experience.
260-
- Of course, we need some testing of this custom tool architecture to know if it can scale to all tools. Also, custom tool registration is only supported with HTTP, so we'll need to support this feature when the stdio protocol is being used.
260+
- Of course, we need some testing of this custom tool architecture to know if it can scale to all tools. ~~Also, custom tool registration is only supported with HTTP, so we'll need to support this feature when the stdio protocol is being used.~~
261+
- Custom tools now work in both HTTP and stdio transports.

0 commit comments

Comments
 (0)