Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion MCPForUnity/Editor/Services/ToolDiscoveryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,30 @@ public List<ToolMetadata> DiscoverAllTools()

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

// Primary scan via TypeCache (fast, but can miss project assemblies in some domain-reload states)
var toolTypes = TypeCache.GetTypesWithAttribute<McpForUnityToolAttribute>();
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<McpForUnityToolAttribute>() != null);

// Merge both scans, deduplicating by type
var allToolTypes = toolTypes
.Concat(appDomainTypes)
.Distinct()
.ToList();

foreach (var type in allToolTypes)
{
McpForUnityToolAttribute toolAttr;
try
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
24 changes: 23 additions & 1 deletion Server/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion Server/src/models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand All @@ -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,
}
5 changes: 3 additions & 2 deletions Server/src/services/custom_tool_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion Server/src/transport/legacy/port_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion docs/migrations/v8_NEW_NETWORKING_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.