Skip to content
Closed
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
10 changes: 0 additions & 10 deletions MCPForUnity/Editor/Services/ToolDiscoveryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -226,16 +226,6 @@ private void EnsurePreferenceInitialized(ToolMetadata metadata)
{
bool defaultValue = metadata.AutoRegister || metadata.IsBuiltIn;
EditorPrefs.SetBool(key, defaultValue);
return;
}

if (metadata.IsBuiltIn && !metadata.AutoRegister)
{
bool currentValue = EditorPrefs.GetBool(key, metadata.AutoRegister);
if (currentValue == metadata.AutoRegister)
{
EditorPrefs.SetBool(key, true);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ public interface IMcpTransportClient
Task<bool> StartAsync();
Task StopAsync();
Task<bool> VerifyAsync();
Task ReregisterToolsAsync();
}
}
24 changes: 14 additions & 10 deletions MCPForUnity/Editor/Services/Transport/TransportManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,6 @@ private IMcpTransportClient GetOrCreateClient(TransportMode mode)
};
}

private IMcpTransportClient GetClient(TransportMode mode)
{
return mode switch
{
TransportMode.Http => _httpClient,
TransportMode.Stdio => _stdioClient,
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "Unsupported transport mode"),
};
}

public async Task<bool> StartAsync(TransportMode mode)
{
IMcpTransportClient client = GetOrCreateClient(mode);
Expand Down Expand Up @@ -128,6 +118,20 @@ public TransportState GetState(TransportMode mode)

public bool IsRunning(TransportMode mode) => GetState(mode).IsConnected;

/// <summary>
/// Gets the active transport client for the specified mode.
/// Returns null if the client hasn't been created yet.
/// </summary>
public IMcpTransportClient GetClient(TransportMode mode)
{
return mode switch
{
TransportMode.Http => _httpClient,
TransportMode.Stdio => _stdioClient,
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "Unsupported transport mode"),
};
}

private void UpdateState(TransportMode mode, TransportState state)
{
switch (mode)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,12 @@ public Task<bool> VerifyAsync()
return Task.FromResult(running);
}

public Task ReregisterToolsAsync()
{
// Stdio transport doesn't support dynamic tool reregistration
// Tools are registered at server startup
return Task.CompletedTask;
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,29 @@ private async Task SendRegisterToolsAsync(CancellationToken token)
McpLog.Info($"[WebSocket] Sent {tools.Count} tools registration", false);
}

public async Task ReregisterToolsAsync()
{
if (!IsConnected || _lifecycleCts == null)
{
McpLog.Warn("[WebSocket] Cannot reregister tools: not connected");
return;
}

try
{
await SendRegisterToolsAsync(_lifecycleCts.Token).ConfigureAwait(false);
McpLog.Info("[WebSocket] Tool reregistration completed", false);
}
catch (System.OperationCanceledException)
{
McpLog.Warn("[WebSocket] Tool reregistration cancelled");
}
catch (System.Exception ex)
{
McpLog.Error($"[WebSocket] Tool reregistration failed: {ex.Message}");
}
}
Comment on lines +509 to +530
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Handle potential races where _lifecycleCts is disposed between the initial check and use.

In ReregisterToolsAsync, there’s a race between the _lifecycleCts != null check and SendRegisterToolsAsync(_lifecycleCts.Token): if the CTS is disposed in between (e.g., during shutdown), you may get ObjectDisposedException instead of OperationCanceledException.

To harden this:

  • capture _lifecycleCts into a local and use that consistently, and
  • consider handling ObjectDisposedException the same as OperationCanceledException (treat as benign cancellation rather than an error) to avoid noisy shutdown logs.
Suggested change
public async Task ReregisterToolsAsync()
{
if (!IsConnected || _lifecycleCts == null)
{
McpLog.Warn("[WebSocket] Cannot reregister tools: not connected");
return;
}
try
{
await SendRegisterToolsAsync(_lifecycleCts.Token).ConfigureAwait(false);
McpLog.Info("[WebSocket] Tool reregistration completed", false);
}
catch (System.OperationCanceledException)
{
McpLog.Warn("[WebSocket] Tool reregistration cancelled");
}
catch (System.Exception ex)
{
McpLog.Error($"[WebSocket] Tool reregistration failed: {ex.Message}");
}
}
public async Task ReregisterToolsAsync()
{
// Capture CTS locally to avoid races with concurrent disposal/change
var lifecycleCts = _lifecycleCts;
if (!IsConnected || lifecycleCts == null)
{
McpLog.Warn("[WebSocket] Cannot reregister tools: not connected");
return;
}
try
{
await SendRegisterToolsAsync(lifecycleCts.Token).ConfigureAwait(false);
McpLog.Info("[WebSocket] Tool reregistration completed", false);
}
catch (System.OperationCanceledException)
{
McpLog.Warn("[WebSocket] Tool reregistration cancelled");
}
catch (System.ObjectDisposedException)
{
// Treat disposal during shutdown the same as cancellation to avoid noisy logs
McpLog.Warn("[WebSocket] Tool reregistration cancelled");
}
catch (System.Exception ex)
{
McpLog.Error($"[WebSocket] Tool reregistration failed: {ex.Message}");
}
}


private async Task HandleExecuteAsync(JObject payload, CancellationToken token)
{
string commandId = payload.Value<string>("id");
Expand Down
53 changes: 50 additions & 3 deletions MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Services;
using MCPForUnity.Editor.Services.Transport;
using MCPForUnity.Editor.Tools;
using UnityEditor;
using UnityEngine.UIElements;
Expand Down Expand Up @@ -223,23 +225,61 @@ private VisualElement CreateToolRow(ToolMetadata tool)
return row;
}

private void HandleToggleChange(ToolMetadata tool, bool enabled, bool updateSummary = true)
private void HandleToggleChange(
ToolMetadata tool,
bool enabled,
bool updateSummary = true,
bool reregisterTools = true)
{
MCPServiceLocator.ToolDiscovery.SetToolEnabled(tool.Name, enabled);

if (updateSummary)
{
UpdateSummary();
}

if (reregisterTools)
{
// Trigger tool reregistration with connected MCP server
ReregisterToolsAsync();
}
}

private void ReregisterToolsAsync()
{
// Fire and forget - don't block UI
ThreadPool.QueueUserWorkItem(_ =>
{
try
{
var transportManager = MCPServiceLocator.TransportManager;
var client = transportManager.GetClient(TransportMode.Http);
if (client != null && client.IsConnected)
{
client.ReregisterToolsAsync().Wait();
}
}
catch (Exception ex)
{
McpLog.Warn($"Failed to reregister tools: {ex.Message}");
Comment on lines +259 to +264
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ReregisterToolsAsync() blocks on an async call using .Wait(). This can wrap exceptions in AggregateException (often resulting in an unhelpful One or more errors occurred message) and unnecessarily blocks a ThreadPool thread. Prefer an async flow (e.g., Task.Run(async () => await client.ReregisterToolsAsync())) or at least use GetAwaiter().GetResult() and log the inner exception details.

Suggested change
client.ReregisterToolsAsync().Wait();
}
}
catch (Exception ex)
{
McpLog.Warn($"Failed to reregister tools: {ex.Message}");
// Use GetAwaiter().GetResult() to avoid AggregateException wrapping
client.ReregisterToolsAsync().GetAwaiter().GetResult();
}
}
catch (Exception ex)
{
// Log full exception details, including any inner exceptions
if (ex is AggregateException aggregateException && aggregateException.InnerException != null)
{
McpLog.Warn($"Failed to reregister tools: {aggregateException.InnerException}");
}
else
{
McpLog.Warn($"Failed to reregister tools: {ex}");
}

Copilot uses AI. Check for mistakes.
}
});
}
Comment on lines +248 to 267
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if BridgeService exposes preferred mode publicly and how Stdio transport implements ReregisterToolsAsync
echo "=== BridgeControlService public API ==="
rg -n "PreferredMode|_preferredMode|ResolvePreferredMode" --type=cs -C2

echo ""
echo "=== StdioTransportClient.ReregisterToolsAsync ==="
ast-grep --pattern $'class StdioTransportClient {
  $$$
  ReregisterToolsAsync() {
    $$$
  }
  $$$
}'

echo ""
echo "=== All ReregisterToolsAsync implementations ==="
rg -n "ReregisterToolsAsync" --type=cs -C3

Repository: CoplayDev/unity-mcp

Length of output: 9942


🏁 Script executed:

#!/bin/bash
echo "=== MCPServiceLocator API ==="
rg -n "class MCPServiceLocator|public static.*BridgeService|BridgeService.*get" --type=cs -A2 -B2

echo ""
echo "=== TransportManager.GetClient implementation ==="
ast-grep --pattern $'GetClient($MODE) {
  $$$
}'

echo ""
echo "=== How GetClient is used in other places ==="
rg -n "GetClient\(" --type=cs -B2 -A2 | head -80

Repository: CoplayDev/unity-mcp

Length of output: 2246


🏁 Script executed:

#!/bin/bash
echo "=== MCPServiceLocator full implementation ==="
cat -n MCPForUnity/Editor/Services/MCPServiceLocator.cs

echo ""
echo "=== TransportManager.GetClient full implementation ==="
rg -n "public IMcpTransportClient GetClient" --type=cs -A10

Repository: CoplayDev/unity-mcp

Length of output: 7093


🏁 Script executed:

#!/bin/bash
echo "=== IBridgeControlService interface ==="
rg -n "interface IBridgeControlService" --type=cs -A20

echo ""
echo "=== TransportMode enum ==="
rg -n "enum TransportMode" --type=cs -A10

echo ""
echo "=== Checking if WebSocketTransportClient is actually used ==="
rg -n "WebSocketTransportClient|WebSocket" --type=cs | head -20

Repository: CoplayDev/unity-mcp

Length of output: 5587


Use the active transport mode instead of hardcoding Http.

ReregisterToolsAsync always targets TransportMode.Http (line 256), but this ignores the user's configured transport preference. If the user has configured Stdio as the active transport (via BridgeControlService.ResolvePreferredMode), the Http client may not be initialized, causing GetClient to return null and reregistration to silently fail.

Use MCPServiceLocator.Bridge.ActiveMode to respect the user's configuration:

Proposed fix
 private void ReregisterToolsAsync()
 {
     // Fire and forget - don't block UI
     ThreadPool.QueueUserWorkItem(_ =>
     {
         try
         {
             var transportManager = MCPServiceLocator.TransportManager;
-            var client = transportManager.GetClient(TransportMode.Http);
+            var mode = MCPServiceLocator.Bridge.ActiveMode;
+            if (!mode.HasValue)
+                return;
+            var client = transportManager.GetClient(mode.Value);
             if (client != null && client.IsConnected)
             {
                 client.ReregisterToolsAsync().Wait();
             }
         }
         catch (Exception ex)
         {
             McpLog.Warn($"Failed to reregister tools: {ex.Message}");
         }
     });
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private void ReregisterToolsAsync()
{
// Fire and forget - don't block UI
ThreadPool.QueueUserWorkItem(_ =>
{
try
{
var transportManager = MCPServiceLocator.TransportManager;
var client = transportManager.GetClient(TransportMode.Http);
if (client != null && client.IsConnected)
{
client.ReregisterToolsAsync().Wait();
}
}
catch (Exception ex)
{
McpLog.Warn($"Failed to reregister tools: {ex.Message}");
}
});
}
private void ReregisterToolsAsync()
{
// Fire and forget - don't block UI
ThreadPool.QueueUserWorkItem(_ =>
{
try
{
var transportManager = MCPServiceLocator.TransportManager;
var mode = MCPServiceLocator.Bridge.ActiveMode;
if (!mode.HasValue)
return;
var client = transportManager.GetClient(mode.Value);
if (client != null && client.IsConnected)
{
client.ReregisterToolsAsync().Wait();
}
}
catch (Exception ex)
{
McpLog.Warn($"Failed to reregister tools: {ex.Message}");
}
});
}
🤖 Prompt for AI Agents
In `@MCPForUnity/Editor/Windows/Components/Tools/McpToolsSection.cs` around lines
248 - 267, ReregisterToolsAsync currently hardcodes TransportMode.Http when
calling MCPServiceLocator.TransportManager.GetClient, which ignores the user's
configured transport; change it to use the active transport mode from
MCPServiceLocator.Bridge.ActiveMode when obtaining the client (keep the existing
null and IsConnected checks and the try/catch around
client.ReregisterToolsAsync().Wait()). Update references in the
ReregisterToolsAsync method to call GetClient with
MCPServiceLocator.Bridge.ActiveMode so reregistration respects the user's
preferred transport.


private void SetAllToolsState(bool enabled)
{
bool hasChanges = false;

foreach (var tool in allTools)
{
if (!toolToggleMap.TryGetValue(tool.Name, out var toggle))
{
MCPServiceLocator.ToolDiscovery.SetToolEnabled(tool.Name, enabled);
bool currentEnabled = MCPServiceLocator.ToolDiscovery.IsToolEnabled(tool.Name);
if (currentEnabled != enabled)
{
MCPServiceLocator.ToolDiscovery.SetToolEnabled(tool.Name, enabled);
hasChanges = true;
}
continue;
}

Expand All @@ -249,10 +289,17 @@ private void SetAllToolsState(bool enabled)
}

toggle.SetValueWithoutNotify(enabled);
HandleToggleChange(tool, enabled, updateSummary: false);
HandleToggleChange(tool, enabled, updateSummary: false, reregisterTools: false);
hasChanges = true;
}

UpdateSummary();

if (hasChanges)
{
// Trigger a single reregistration after bulk change
ReregisterToolsAsync();
}
}

private void UpdateSummary()
Expand Down
142 changes: 142 additions & 0 deletions Server/src/transport/unity_instance_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,43 @@ def __init__(self):
super().__init__()
self._active_by_key: dict[str, str] = {}
self._lock = RLock()
self._unity_managed_tool_names = {
"batch_execute",
"execute_menu_item",
"find_gameobjects",
"get_test_job",
"manage_asset",
"manage_components",
"manage_editor",
"manage_gameobject",
"manage_material",
"manage_prefabs",
"manage_scene",
"manage_script",
"manage_scriptable_object",
"manage_shader",
"manage_texture",
"manage_vfx",
"read_console",
"refresh_unity",
"run_tests",
}
self._tool_alias_to_unity_target = {
# Server-side script helpers route to Unity's manage_script command.
"apply_text_edits": "manage_script",
"create_script": "manage_script",
"delete_script": "manage_script",
"find_in_file": "manage_script",
"get_sha": "manage_script",
"script_apply_edits": "manage_script",
"validate_script": "manage_script",
}
self._server_only_tool_names = {
"debug_request_context",
"execute_custom_tool",
"manage_script_capabilities",
"set_active_instance",
}

def get_session_key(self, ctx) -> str:
"""
Expand Down Expand Up @@ -260,3 +297,108 @@ async def on_read_resource(self, context: MiddlewareContext, call_next):
"""Inject active Unity instance into resource context if available."""
await self._inject_unity_instance(context)
return await call_next(context)

async def on_list_tools(self, context: MiddlewareContext, call_next):
"""Filter MCP tool listing to the Unity-enabled set when session data is available."""
await self._inject_unity_instance(context)
tools = await call_next(context)

if not self._should_filter_tool_listing():
return tools

enabled_tool_names = await self._resolve_enabled_tool_names_for_context(context)
if not enabled_tool_names:
return tools

filtered = []
for tool in tools:
tool_name = getattr(tool, "name", None)
if self._is_tool_visible(tool_name, enabled_tool_names):
filtered.append(tool)

return filtered

def _should_filter_tool_listing(self) -> bool:
transport = (config.transport_mode or "stdio").lower()
return transport == "http" and PluginHub.is_configured()

async def _resolve_enabled_tool_names_for_context(
self,
context: MiddlewareContext,
) -> set[str] | None:
ctx = context.fastmcp_context
user_id = ctx.get_state("user_id") if config.http_remote_hosted else None
active_instance = ctx.get_state("unity_instance")

project_hashes = self._resolve_candidate_project_hashes(active_instance)
if not project_hashes:
try:
sessions_data = await PluginHub.get_sessions(user_id=user_id)
sessions = sessions_data.sessions if sessions_data else {}
except Exception:
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The except Exception: return None around PluginHub.get_sessions(...) swallows all errors without logging. This can make it hard to understand why tool filtering is skipped, and can hide real bugs. Consider logging (debug/warn) and re-raising SystemExit/KeyboardInterrupt (consistent with the rest of this file).

Suggested change
except Exception:
except (KeyboardInterrupt, SystemExit):
# Preserve process-control semantics.
raise
except Exception as exc:
logger.warning(
"Failed to fetch sessions from PluginHub for user %r; "
"skipping tool filtering: %s",
user_id,
exc,
)

Copilot uses AI. Check for mistakes.
return None
Comment on lines +335 to +339
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Silent exception swallowing hides bugs.

The bare except Exception: return None at line 338 will silently swallow programming errors (e.g., TypeError, KeyError from unexpected data shapes). At minimum, log the exception at debug level so it's diagnosable.

Proposed fix
             try:
                 sessions_data = await PluginHub.get_sessions(user_id=user_id)
                 sessions = sessions_data.sessions if sessions_data else {}
-            except Exception:
+            except Exception as exc:
+                logger.debug(
+                    "Failed to fetch sessions for tool filtering: %s",
+                    exc,
+                    exc_info=True,
+                )
                 return None
🧰 Tools
🪛 Ruff (0.15.0)

[warning] 338-338: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
In `@Server/src/transport/unity_instance_middleware.py` around lines 335 - 339,
The try/except around PluginHub.get_sessions currently swallows all exceptions;
change it to "except Exception as e:" and log the exception at debug (or
debug/exception) level before returning None so programming errors are visible;
specifically update the block around PluginHub.get_sessions/user_id to capture
the exception into a variable (e) and call the module's logger (e.g.,
logger.debug or logger.exception) with a clear message including e and any
contextual info (user_id, sessions_data) prior to returning None.


if not sessions:
return None

if len(sessions) == 1:
only_session = next(iter(sessions.values()))
only_hash = getattr(only_session, "hash", None)
if only_hash:
project_hashes = [only_hash]
else:
# Multiple sessions without explicit selection: use a union so we don't
# hide tools that are valid in at least one visible Unity instance.
project_hashes = [
session.hash
Comment on lines +352 to +353
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Guard against missing hash attribute when building project_hashes for multiple sessions.

In this comprehension, session.hash is accessed before the if condition is evaluated, so a session without a hash attribute will still raise AttributeError. Use getattr for both access and filtering, e.g.:

project_hashes = [
    h
    for session in sessions.values()
    if (h := getattr(session, "hash", None))
]

or an equivalent version without the walrus operator.

for session in sessions.values()
if getattr(session, "hash", None)
]

if not project_hashes:
return None

enabled_tool_names: set[str] = set()
for project_hash in project_hashes:
try:
registered_tools = await PluginHub.get_tools_for_project(project_hash)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Scope tool lookup to the current user in hosted mode

In remote-hosted deployments, this lookup ignores user_id even though _resolve_enabled_tool_names_for_context already derived it, so filtering uses PluginHub.get_tools_for_project(project_hash) with only a hash. PluginRegistry stores hosted sessions under (user_id, project_hash), which means hash-only resolution can return no session and the enabled set stays empty; on_list_tools then falls back to returning the full unfiltered tool list for that user.

Useful? React with 👍 / 👎.

except Exception:
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The except Exception: continue around PluginHub.get_tools_for_project(...) silently drops failures and can yield an empty enabled-tool set, causing the middleware to return the unfiltered tool list. Consider logging the exception (at least debug) so failures don't silently negate filtering behavior.

Suggested change
except Exception:
except Exception:
logger.debug(
"Failed to fetch tools for project %s in UnityInstanceMiddleware; skipping this project.",
project_hash,
exc_info=True,
)

Copilot uses AI. Check for mistakes.
continue

for tool in registered_tools:
tool_name = getattr(tool, "name", None)
if isinstance(tool_name, str) and tool_name:
enabled_tool_names.add(tool_name)
Comment on lines +362 to +371
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

try/except/continue silently discards errors — add logging.

Ruff S112 flags this pattern. If get_tools_for_project fails for one hash due to a bug (not a transient network error), this silently skips it, making issues very hard to diagnose.

Proposed fix
         for project_hash in project_hashes:
             try:
                 registered_tools = await PluginHub.get_tools_for_project(project_hash)
-            except Exception:
+            except Exception as exc:
+                logger.debug(
+                    "Failed to get tools for project hash %s: %s",
+                    project_hash,
+                    exc,
+                    exc_info=True,
+                )
                 continue
🧰 Tools
🪛 Ruff (0.15.0)

[error] 365-366: try-except-continue detected, consider logging the exception

(S112)


[warning] 365-365: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
In `@Server/src/transport/unity_instance_middleware.py` around lines 362 - 371,
The current loop over project_hashes swallows all exceptions from
PluginHub.get_tools_for_project by using bare except and continue; change the
except to capture the exception as e (e.g., except Exception as e) and log the
failure (including project_hash and exception details/traceback) before
continuing so failures are discoverable — update the except block around
PluginHub.get_tools_for_project to call the module or instance logger (e.g.,
logger.exception or logger.error with exc_info=True) referencing project_hash
and the caught exception, then continue to the next hash.


return enabled_tool_names or None

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep filtering when Unity reports zero enabled tools

This converts an empty enabled-tool set into None, and on_list_tools interprets that as 'no data, skip filtering'. If a Unity instance intentionally disables all Unity-managed tools and re-registers an empty tool list, the middleware will incorrectly return the full unfiltered list instead of hiding disabled tools, so tool listing no longer matches Unity’s actual enabled state.

Useful? React with 👍 / 👎.


@staticmethod
def _resolve_candidate_project_hashes(active_instance: str | None) -> list[str]:
if not active_instance:
return []

if "@" in active_instance:
_, _, suffix = active_instance.rpartition("@")
return [suffix] if suffix else []

return [active_instance]

def _is_tool_visible(self, tool_name: str | None, enabled_tool_names: set[str]) -> bool:
if not isinstance(tool_name, str) or not tool_name:
return True

if tool_name in self._server_only_tool_names:
return True

if tool_name in enabled_tool_names:
return True

unity_target = self._tool_alias_to_unity_target.get(tool_name)
if unity_target:
return unity_target in enabled_tool_names

# Keep unknown tools visible for forward compatibility.
if tool_name not in self._unity_managed_tool_names:
return True

return False
Loading
Loading