diff --git a/MCPForUnity/Editor/Services/ToolDiscoveryService.cs b/MCPForUnity/Editor/Services/ToolDiscoveryService.cs index 6a13a5355..e2ff51254 100644 --- a/MCPForUnity/Editor/Services/ToolDiscoveryService.cs +++ b/MCPForUnity/Editor/Services/ToolDiscoveryService.cs @@ -23,8 +23,30 @@ public List DiscoverAllTools() _cachedTools = new Dictionary(); + // Primary scan via TypeCache (fast, but can miss project assemblies in some domain-reload states) var toolTypes = TypeCache.GetTypesWithAttribute(); - foreach (var type in toolTypes) + + // Fallback scan via AppDomain (slower but exhaustive; mirrors CommandRegistry behaviour) + var appDomainTypes = AppDomain.CurrentDomain.GetAssemblies() + .Where(a => !a.IsDynamic) + .SelectMany(a => + { + try { return a.GetTypes(); } + catch (Exception ex) + { + McpLog.Warn($"Failed to reflect types from assembly {a.FullName}: {ex.Message}"); + return new Type[0]; + } + }) + .Where(t => t.GetCustomAttribute() != null); + + // Merge both scans, deduplicating by type + var allToolTypes = toolTypes + .Concat(appDomainTypes) + .Distinct() + .ToList(); + + foreach (var type in allToolTypes) { McpForUnityToolAttribute toolAttr; try diff --git a/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs b/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs index 0e9674d65..8925cff64 100644 --- a/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs +++ b/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs @@ -1047,6 +1047,11 @@ public static void WriteHeartbeat(bool reloading, string reason = null) } catch { } + bool projectScopedTools = EditorPrefs.GetBool( + EditorPrefKeys.ProjectScopedToolsLocalHttp, + true // default to true so stdio behaves like HTTP Local by default + ); + var payload = new { unity_port = currentUnityPort, @@ -1056,7 +1061,8 @@ public static void WriteHeartbeat(bool reloading, string reason = null) project_path = Application.dataPath, project_name = projectName, unity_version = Application.unityVersion, - last_heartbeat = DateTime.UtcNow.ToString("O") + last_heartbeat = DateTime.UtcNow.ToString("O"), + project_scoped_tools = projectScopedTools }; File.WriteAllText(filePath, JsonConvert.SerializeObject(payload), new System.Text.UTF8Encoding(false)); } diff --git a/MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs b/MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs index b7b53f718..bb8980023 100644 --- a/MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs +++ b/MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs @@ -77,7 +77,7 @@ private void RegisterCallbacks() EditorPrefKeys.ProjectScopedToolsLocalHttp, false ); - projectScopedToolsToggle.tooltip = "When enabled, register project-scoped tools with HTTP Local transport. Allows per-project tool customization."; + projectScopedToolsToggle.tooltip = "When enabled, register project-scoped tools with HTTP Local and stdio transports. Allows per-project tool customization."; projectScopedToolsToggle.RegisterValueChangedCallback(evt => { EditorPrefs.SetBool(EditorPrefKeys.ProjectScopedToolsLocalHttp, evt.newValue); diff --git a/Server/src/main.py b/Server/src/main.py index 311d5dc88..1e6b3c18a 100644 --- a/Server/src/main.py +++ b/Server/src/main.py @@ -884,10 +884,32 @@ def main(): if args.http_port: logger.info(f"HTTP port override: {http_port}") - project_scoped_tools = ( + # Explicit CLI/env overrides always win + project_scoped_tools_explicit = ( bool(args.project_scoped_tools) or os.environ.get("UNITY_MCP_PROJECT_SCOPED_TOOLS", "").lower() in ("true", "1", "yes", "on") ) + + # If not explicitly set, check Unity status files for the default instance. + # In stdio mode there is typically only one instance, so "first match wins" is fine. + project_scoped_tools = project_scoped_tools_explicit + if not project_scoped_tools_explicit: + try: + from transport.legacy.unity_connection import get_unity_connection_pool + pool = get_unity_connection_pool() + instances = pool.discover_all_instances() + # If ANY discovered instance requests project-scoped tools, enable them + for inst in instances: + if getattr(inst, "project_scoped_tools", False): + project_scoped_tools = True + logger.info( + "Enabling project-scoped tools because Unity instance %s requested it", + inst.id, + ) + break + except Exception: + logger.debug("Could not discover Unity instances for project-scoped tool default", exc_info=True) + mcp = create_mcp_server(project_scoped_tools) # Determine transport mode diff --git a/Server/src/models/models.py b/Server/src/models/models.py index 3476811c0..0164aa621 100644 --- a/Server/src/models/models.py +++ b/Server/src/models/models.py @@ -42,6 +42,7 @@ class UnityInstanceInfo(BaseModel): status: str # "running", "reloading", "offline" last_heartbeat: datetime | None = None unity_version: str | None = None + project_scoped_tools: bool = False def to_dict(self) -> dict[str, Any]: """Convert to dictionary for JSON serialization""" @@ -53,5 +54,6 @@ def to_dict(self) -> dict[str, Any]: "port": self.port, "status": self.status, "last_heartbeat": self.last_heartbeat.isoformat() if self.last_heartbeat else None, - "unity_version": self.unity_version + "unity_version": self.unity_version, + "project_scoped_tools": self.project_scoped_tools, } diff --git a/Server/src/services/custom_tool_service.py b/Server/src/services/custom_tool_service.py index 33d182569..04394f33e 100644 --- a/Server/src/services/custom_tool_service.py +++ b/Server/src/services/custom_tool_service.py @@ -327,8 +327,9 @@ def _register_project_tools( return registered, replaced def register_global_tools(self, tools: list[ToolDefinitionModel]) -> None: - if self._project_scoped_tools: - return + # Global custom tools are always registered, even when project-scoped tools + # are enabled. Project-scoped tools can override globals by name, but + # disabling globals entirely would break shared tooling that projects expect. builtin_names = self._get_builtin_tool_names() for tool in tools: if tool.name in builtin_names: diff --git a/Server/src/transport/legacy/port_discovery.py b/Server/src/transport/legacy/port_discovery.py index 85a86a69a..0dd757876 100644 --- a/Server/src/transport/legacy/port_discovery.py +++ b/Server/src/transport/legacy/port_discovery.py @@ -309,7 +309,8 @@ def discover_all_unity_instances() -> list[UnityInstanceInfo]: status="reloading" if is_reloading else "running", last_heartbeat=last_heartbeat, # May not be available in current version - unity_version=data.get('unity_version') + unity_version=data.get('unity_version'), + project_scoped_tools=data.get('project_scoped_tools', False), ) instances_by_port[port] = (instance, freshness) diff --git a/docs/migrations/v8_NEW_NETWORKING_SETUP.md b/docs/migrations/v8_NEW_NETWORKING_SETUP.md index 38daf1977..9e96cae62 100644 --- a/docs/migrations/v8_NEW_NETWORKING_SETUP.md +++ b/docs/migrations/v8_NEW_NETWORKING_SETUP.md @@ -257,4 +257,5 @@ This was a big change, and it touches all the repo. So a lot of inefficiencies a - ~~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.~~ - This was done, Server folder is much more hierarchical and structured. - 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. - - 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. + - 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.~~ + - Custom tools now work in both HTTP and stdio transports.