diff --git a/.claude/mcp.json b/.claude/mcp.json
index 63da78661..ae0c7a456 100644
--- a/.claude/mcp.json
+++ b/.claude/mcp.json
@@ -1,16 +1,8 @@
{
"mcpServers": {
"UnityMCP": {
- "type": "stdio",
- "command": "uv",
- "args": [
- "run",
- "--directory",
- "${workspaceFolder}/Server",
- "src/main.py",
- "--transport",
- "stdio"
- ]
+ "type": "http",
+ "url": "http://localhost:8080/mcp"
}
}
}
diff --git a/.claude/settings.json b/.claude/settings.json
index 127026519..29f403bcd 100644
--- a/.claude/settings.json
+++ b/.claude/settings.json
@@ -6,12 +6,6 @@
"MultiEdit(reports/**)"
],
"deny": [
- "WebFetch",
- "WebSearch",
- "Task",
- "TodoWrite",
- "NotebookEdit",
- "NotebookRead"
]
}
}
diff --git a/.gitignore b/.gitignore
index 6cd591730..88246fdde 100644
--- a/.gitignore
+++ b/.gitignore
@@ -55,9 +55,9 @@ TestProjects/UnityMCPTests/Assets/Temp/
# CI test reports (generated during test runs)
reports/
-# Local Claude configs (not for repo)
-.claude/local/
-
# Local testing harness
scripts/local-test/
.claude/settings.local.json
+
+# Ignore the .claude directory, since it might contain local/project-level setting such as deny and allowlist.
+/.claude
diff --git a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
index f2f170736..7b7f9b8cd 100644
--- a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
+++ b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
@@ -543,13 +543,39 @@ public ClaudeCliMcpConfigurator(McpClient client) : base(client) { }
public override string GetConfigPath() => "Managed via Claude CLI";
///
+ /// Returns the project directory that CLI-based configurators will use as the working directory
+ /// for `claude mcp add/remove --scope local`. Checks for an explicit override in EditorPrefs
+ /// first, then falls back to the current Unity project directory.
+ /// The override is useful when the Claude Code workspace is at a different path than the Unity project
+ /// (e.g., plugin developers running CC from the repo root while Unity is open with a test project).
+ /// MUST be called from the main Unity thread (accesses Application.dataPath and EditorPrefs).
+ ///
+ internal static string GetClientProjectDir()
+ {
+ string overrideDir = EditorPrefs.GetString(EditorPrefKeys.ClientProjectDirOverride, string.Empty);
+ if (!string.IsNullOrEmpty(overrideDir) && Directory.Exists(overrideDir))
+ return overrideDir;
+ return Path.GetDirectoryName(Application.dataPath);
+ }
+
+ ///
+ /// Returns true if a valid client project directory override is set.
+ ///
+ internal static bool HasClientProjectDirOverride
+ {
+ get
+ {
+ string overrideDir = EditorPrefs.GetString(EditorPrefKeys.ClientProjectDirOverride, string.Empty);
+ return !string.IsNullOrEmpty(overrideDir) && Directory.Exists(overrideDir);
+ }
+ }
/// Checks the Claude CLI registration status.
/// MUST be called from the main Unity thread due to EditorPrefs and Application.dataPath access.
///
public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
{
// Capture main-thread-only values before delegating to thread-safe method
- string projectDir = Path.GetDirectoryName(Application.dataPath);
+ string projectDir = GetClientProjectDir();
bool useHttpTransport = EditorConfigurationCache.Instance.UseHttpTransport;
// Resolve claudePath on the main thread (EditorPrefs access)
string claudePath = MCPServiceLocator.Paths.GetClaudeCliPath();
@@ -557,7 +583,7 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
bool isRemoteScope = HttpEndpointUtility.IsRemoteScope();
// Get expected package source for the installed package version (matches what Register() would use)
string expectedPackageSource = GetExpectedPackageSourceForValidation();
- return CheckStatusWithProjectDir(projectDir, useHttpTransport, claudePath, platform, isRemoteScope, expectedPackageSource, attemptAutoRewrite);
+ return CheckStatusWithProjectDir(projectDir, useHttpTransport, claudePath, platform, isRemoteScope, expectedPackageSource, attemptAutoRewrite, HasClientProjectDirOverride);
}
///
@@ -572,7 +598,7 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
internal McpStatus CheckStatusWithProjectDir(
string projectDir, bool useHttpTransport, string claudePath, RuntimePlatform platform,
bool isRemoteScope, string expectedPackageSource,
- bool attemptAutoRewrite = false)
+ bool attemptAutoRewrite = false, bool hasProjectDirOverride = false)
{
try
{
@@ -632,8 +658,12 @@ internal McpStatus CheckStatusWithProjectDir(
client.configuredTransport = Models.ConfiguredTransport.Unknown;
}
- // Check for transport mismatch
- bool hasTransportMismatch = (currentUseHttp && registeredWithStdio) || (!currentUseHttp && registeredWithHttp);
+ // Check for transport mismatch.
+ // When a project dir override is active, the local UseHttpTransport
+ // GUI setting may legitimately differ from the registered transport
+ // in the overridden project, so skip this check.
+ bool hasTransportMismatch = !hasProjectDirOverride
+ && ((currentUseHttp && registeredWithStdio) || (!currentUseHttp && registeredWithHttp));
// For stdio transport, also check package version
bool hasVersionMismatch = false;
@@ -873,7 +903,7 @@ private void Register()
args = $"mcp add --scope local --transport stdio UnityMCP -- \"{uvxPath}\" {devFlags}{fromArgs} {packageName}";
}
- string projectDir = Path.GetDirectoryName(Application.dataPath);
+ string projectDir = GetClientProjectDir();
string pathPrepend = null;
if (Application.platform == RuntimePlatform.OSXEditor)
@@ -925,7 +955,7 @@ private void Unregister()
throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first.");
}
- string projectDir = Path.GetDirectoryName(Application.dataPath);
+ string projectDir = GetClientProjectDir();
string pathPrepend = null;
if (Application.platform == RuntimePlatform.OSXEditor)
{
@@ -997,6 +1027,7 @@ public override string GetManualSnippet()
///
/// Removes UnityMCP registration from all Claude Code configuration scopes (local, user, project).
+ /// Also removes legacy entries from ~/.claude.json that the CLI scoped removal can't touch.
/// This ensures no stale or conflicting configurations remain across different scopes.
/// Also handles legacy "unityMCP" naming convention.
///
@@ -1015,6 +1046,81 @@ private static void RemoveFromAllScopes(string claudePath, string projectDir, st
ExecPath.TryRun(claudePath, $"mcp remove --scope {scope} {name}", projectDir, out _, out _, 5000, pathPrepend);
}
}
+
+ // Also remove legacy entries directly from ~/.claude.json.
+ // Older versions and manual CLI commands without --scope wrote mcpServers entries
+ // into the projects section of ~/.claude.json. The scoped `claude mcp remove` commands
+ // above won't touch these, leaving stale/conflicting configs behind.
+ RemoveLegacyUserConfigEntries(projectDir);
+ }
+
+ ///
+ /// Removes UnityMCP entries from the projects section of ~/.claude.json.
+ /// These are legacy entries that were created by older versions or manual commands
+ /// that didn't use --scope. The scoped `claude mcp remove` commands don't clean these up.
+ ///
+ private static void RemoveLegacyUserConfigEntries(string projectDir)
+ {
+ try
+ {
+ string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+ string configPath = Path.Combine(homeDir, ".claude.json");
+ if (!File.Exists(configPath))
+ return;
+
+ string json = File.ReadAllText(configPath);
+ var config = JObject.Parse(json);
+ var projects = config["projects"] as JObject;
+ if (projects == null)
+ return;
+
+ string normalizedProjectDir = NormalizePath(projectDir);
+ bool modified = false;
+
+ // Walk all project entries looking for ones that match our project path
+ foreach (var project in projects.Properties())
+ {
+ string normalizedKey = NormalizePath(project.Name);
+
+ // Match exact path or parent paths (same logic as ReadUserScopeConfig)
+ if (!string.Equals(normalizedKey, normalizedProjectDir, StringComparison.OrdinalIgnoreCase))
+ {
+ // Also check if projectDir is a child of this config entry
+ if (!normalizedProjectDir.StartsWith(normalizedKey + "/", StringComparison.OrdinalIgnoreCase))
+ continue;
+ }
+
+ var mcpServers = project.Value?["mcpServers"] as JObject;
+ if (mcpServers == null)
+ continue;
+
+ // Remove UnityMCP/unityMCP entries (case-insensitive)
+ var toRemove = new List();
+ foreach (var server in mcpServers.Properties())
+ {
+ if (string.Equals(server.Name, "UnityMCP", StringComparison.OrdinalIgnoreCase))
+ {
+ toRemove.Add(server.Name);
+ }
+ }
+
+ foreach (var name in toRemove)
+ {
+ mcpServers.Remove(name);
+ modified = true;
+ McpLog.Info($"Removed legacy '{name}' entry from ~/.claude.json for project '{project.Name}'");
+ }
+ }
+
+ if (modified)
+ {
+ File.WriteAllText(configPath, config.ToString(Formatting.Indented));
+ }
+ }
+ catch (Exception ex)
+ {
+ McpLog.Warn($"Failed to clean up legacy ~/.claude.json entries: {ex.Message}");
+ }
}
///
@@ -1095,32 +1201,89 @@ private static string ExtractPackageSourceFromCliOutput(string cliOutput)
}
///
- /// Reads Claude Code configuration directly from ~/.claude.json file.
+ /// Reads Claude Code configuration from both local-scope (.claude/mcp.json in the project)
+ /// and user-scope (~/.claude.json). Local scope takes precedence, matching Claude Code's
+ /// own config resolution order.
/// This is much faster than running `claude mcp list` which does health checks on all servers.
///
private static (JObject serverConfig, string error) ReadClaudeCodeConfig(string projectDir)
{
try
{
- // Find the Claude config file
+ // 1. Check local-scope config first: {projectDir}/.claude/mcp.json
+ // This is where `claude mcp add --scope local` writes.
+ var localResult = ReadLocalScopeConfig(projectDir);
+ if (localResult.serverConfig != null)
+ return localResult;
+ if (localResult.error != null)
+ return localResult;
+
+ // 2. Fall back to user-scope config: ~/.claude.json
+ return ReadUserScopeConfig(projectDir);
+ }
+ catch (Exception ex)
+ {
+ return (null, $"Error reading Claude config: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Reads UnityMCP config from the local-scope file: {projectDir}/.claude/mcp.json.
+ /// This is where `claude mcp add --scope local` stores registrations.
+ ///
+ private static (JObject serverConfig, string error) ReadLocalScopeConfig(string projectDir)
+ {
+ try
+ {
+ if (string.IsNullOrEmpty(projectDir))
+ return (null, null);
+
+ string localConfigPath = Path.Combine(projectDir, ".claude", "mcp.json");
+ if (!File.Exists(localConfigPath))
+ return (null, null);
+
+ string json = File.ReadAllText(localConfigPath);
+ var config = JObject.Parse(json);
+ var mcpServers = config["mcpServers"] as JObject;
+ if (mcpServers == null)
+ return (null, null);
+
+ foreach (var server in mcpServers.Properties())
+ {
+ if (string.Equals(server.Name, "UnityMCP", StringComparison.OrdinalIgnoreCase))
+ {
+ return (server.Value as JObject, null);
+ }
+ }
+
+ return (null, null);
+ }
+ catch (Exception ex)
+ {
+ return (null, $"Error reading local Claude config: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Reads UnityMCP config from the user-scope file: ~/.claude.json (projects section).
+ /// This handles legacy configurations and direct user-level entries.
+ ///
+ private static (JObject serverConfig, string error) ReadUserScopeConfig(string projectDir)
+ {
+ try
+ {
string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
string configPath = Path.Combine(homeDir, ".claude.json");
if (!File.Exists(configPath))
- {
- // Missing config file is "not configured", not an error
- // (Claude Code may not be installed or just hasn't been configured yet)
return (null, null);
- }
string configJson = File.ReadAllText(configPath);
var config = JObject.Parse(configJson);
var projects = config["projects"] as JObject;
if (projects == null)
- {
- return (null, null); // No projects configured
- }
+ return (null, null);
// Build a dictionary of normalized paths for quick lookup
// Use last entry for duplicates (forward/backslash variants) as it's typically more recent
@@ -1142,7 +1305,6 @@ private static (JObject serverConfig, string error) ReadClaudeCodeConfig(string
var mcpServers = projectConfig?["mcpServers"] as JObject;
if (mcpServers != null)
{
- // Look for UnityMCP (case-insensitive)
foreach (var server in mcpServers.Properties())
{
if (string.Equals(server.Name, "UnityMCP", StringComparison.OrdinalIgnoreCase))
@@ -1162,11 +1324,11 @@ private static (JObject serverConfig, string error) ReadClaudeCodeConfig(string
currentDir = currentDir.Substring(0, lastSlash);
}
- return (null, null); // Project not found in config
+ return (null, null);
}
catch (Exception ex)
{
- return (null, $"Error reading Claude config: {ex.Message}");
+ return (null, $"Error reading user Claude config: {ex.Message}");
}
}
diff --git a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs
index c5dab7c41..a7e150474 100644
--- a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs
+++ b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs
@@ -22,6 +22,7 @@ internal static class EditorPrefKeys
internal const string UvxPathOverride = "MCPForUnity.UvxPath";
internal const string ClaudeCliPathOverride = "MCPForUnity.ClaudeCliPath";
+ internal const string ClientProjectDirOverride = "MCPForUnity.ClientProjectDir";
internal const string HttpBaseUrl = "MCPForUnity.HttpUrl";
internal const string HttpRemoteBaseUrl = "MCPForUnity.HttpRemoteUrl";
diff --git a/MCPForUnity/Editor/Helpers/ExecPath.cs b/MCPForUnity/Editor/Helpers/ExecPath.cs
index 3801a03a7..350689821 100644
--- a/MCPForUnity/Editor/Helpers/ExecPath.cs
+++ b/MCPForUnity/Editor/Helpers/ExecPath.cs
@@ -52,9 +52,14 @@ internal static string ResolveClaude()
// Common npm global locations
string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty;
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty;
+ string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
string[] candidates =
{
- // Prefer .cmd (most reliable from non-interactive processes)
+ // Native installer locations
+ Path.Combine(localAppData, "Programs", "claude", "claude.exe"),
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "claude", "claude.exe"),
+ Path.Combine(home, ".local", "bin", "claude.exe"),
+ // npm global locations (.cmd preferred for non-interactive processes)
Path.Combine(appData, "npm", "claude.cmd"),
Path.Combine(localAppData, "npm", "claude.cmd"),
// Fall back to PowerShell shim if only .ps1 is present
@@ -277,7 +282,7 @@ private static string Which(string exe, string prependPath)
#endif
#if UNITY_EDITOR_WIN
- private static string FindInPathWindows(string exe, string extraPathPrepend = null)
+ internal static string FindInPathWindows(string exe, string extraPathPrepend = null)
{
try
{
diff --git a/MCPForUnity/Editor/Resources/Editor/ToolStates.cs b/MCPForUnity/Editor/Resources/Editor/ToolStates.cs
new file mode 100644
index 000000000..9ca626121
--- /dev/null
+++ b/MCPForUnity/Editor/Resources/Editor/ToolStates.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Linq;
+using MCPForUnity.Editor.Helpers;
+using MCPForUnity.Editor.Services;
+using Newtonsoft.Json.Linq;
+
+namespace MCPForUnity.Editor.Resources.Editor
+{
+ ///
+ /// Returns the enabled/disabled state of all discovered tools, grouped by group name.
+ /// Used by the Python server (especially in stdio mode) to sync tool visibility.
+ ///
+ [McpForUnityResource("get_tool_states")]
+ public static class ToolStates
+ {
+ public static object HandleCommand(JObject @params)
+ {
+ try
+ {
+ var discovery = MCPServiceLocator.ToolDiscovery;
+ var allTools = discovery.DiscoverAllTools();
+
+ var toolsArray = new JArray();
+ foreach (var tool in allTools)
+ {
+ toolsArray.Add(new JObject
+ {
+ ["name"] = tool.Name,
+ ["group"] = tool.Group ?? "core",
+ ["enabled"] = discovery.IsToolEnabled(tool.Name)
+ });
+ }
+
+ var groups = allTools
+ .GroupBy(t => t.Group ?? "core")
+ .Select(g => new JObject
+ {
+ ["name"] = g.Key,
+ ["enabled_count"] = g.Count(t => discovery.IsToolEnabled(t.Name)),
+ ["total_count"] = g.Count()
+ });
+
+ var result = new JObject
+ {
+ ["tools"] = toolsArray,
+ ["groups"] = new JArray(groups)
+ };
+
+ return new SuccessResponse("Retrieved tool states.", result);
+ }
+ catch (Exception e)
+ {
+ return new ErrorResponse($"Failed to retrieve tool states: {e.Message}");
+ }
+ }
+ }
+}
diff --git a/MCPForUnity/Editor/Resources/Editor/ToolStates.cs.meta b/MCPForUnity/Editor/Resources/Editor/ToolStates.cs.meta
new file mode 100644
index 000000000..25da1749e
--- /dev/null
+++ b/MCPForUnity/Editor/Resources/Editor/ToolStates.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 0f77d36b37ba4526ad30b3c84e3e752c
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/MCPForUnity/Editor/Services/IToolDiscoveryService.cs b/MCPForUnity/Editor/Services/IToolDiscoveryService.cs
index ee2c616b7..090b366c7 100644
--- a/MCPForUnity/Editor/Services/IToolDiscoveryService.cs
+++ b/MCPForUnity/Editor/Services/IToolDiscoveryService.cs
@@ -18,6 +18,7 @@ public class ToolMetadata
public bool RequiresPolling { get; set; } = false;
public string PollAction { get; set; } = "status";
public bool IsBuiltIn { get; set; }
+ public string Group { get; set; } = "core";
}
///
diff --git a/MCPForUnity/Editor/Services/PathResolverService.cs b/MCPForUnity/Editor/Services/PathResolverService.cs
index 4181cf4c6..94adb88e5 100644
--- a/MCPForUnity/Editor/Services/PathResolverService.cs
+++ b/MCPForUnity/Editor/Services/PathResolverService.cs
@@ -114,13 +114,22 @@ public string GetClaudeCliPath()
{
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", "claude", "claude.exe"),
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "claude", "claude.exe"),
- "claude.exe"
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "bin", "claude.exe"),
};
foreach (var c in candidates)
{
if (File.Exists(c)) return c;
}
+
+#if UNITY_EDITOR_WIN
+ // Fall back to PATH search (handles non-standard install locations and npm shims)
+ foreach (var name in new[] { "claude.exe", "claude.cmd", "claude.ps1" })
+ {
+ string fromPath = ExecPath.FindInPathWindows(name);
+ if (!string.IsNullOrEmpty(fromPath)) return fromPath;
+ }
+#endif
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
diff --git a/MCPForUnity/Editor/Services/ToolDiscoveryService.cs b/MCPForUnity/Editor/Services/ToolDiscoveryService.cs
index ac7cc1455..b7ef5d1eb 100644
--- a/MCPForUnity/Editor/Services/ToolDiscoveryService.cs
+++ b/MCPForUnity/Editor/Services/ToolDiscoveryService.cs
@@ -132,7 +132,8 @@ private ToolMetadata ExtractToolMetadata(Type type, McpForUnityToolAttribute too
AssemblyName = type.Assembly.GetName().Name,
AutoRegister = toolAttr.AutoRegister,
RequiresPolling = toolAttr.RequiresPolling,
- PollAction = string.IsNullOrEmpty(toolAttr.PollAction) ? "status" : toolAttr.PollAction
+ PollAction = string.IsNullOrEmpty(toolAttr.PollAction) ? "status" : toolAttr.PollAction,
+ Group = toolAttr.Group ?? "core"
};
metadata.IsBuiltIn = StringCaseUtility.IsBuiltInMcpType(
diff --git a/MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs b/MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs
index 84904d69a..62464d432 100644
--- a/MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs
+++ b/MCPForUnity/Editor/Services/Transport/Transports/WebSocketTransportClient.cs
@@ -540,7 +540,8 @@ private async Task SendRegisterToolsAsync(CancellationToken token)
["description"] = tool.Description,
["structured_output"] = tool.StructuredOutput,
["requires_polling"] = tool.RequiresPolling,
- ["poll_action"] = tool.PollAction
+ ["poll_action"] = tool.PollAction,
+ ["group"] = string.IsNullOrWhiteSpace(tool.Group) ? "core" : tool.Group
};
var paramsArray = new JArray();
diff --git a/MCPForUnity/Editor/Tools/Animation/ManageAnimation.cs b/MCPForUnity/Editor/Tools/Animation/ManageAnimation.cs
index 98f737249..508994009 100644
--- a/MCPForUnity/Editor/Tools/Animation/ManageAnimation.cs
+++ b/MCPForUnity/Editor/Tools/Animation/ManageAnimation.cs
@@ -7,7 +7,7 @@
namespace MCPForUnity.Editor.Tools.Animation
{
- [McpForUnityTool("manage_animation", AutoRegister = false)]
+ [McpForUnityTool("manage_animation", AutoRegister = false, Group = "animation")]
public static class ManageAnimation
{
private static readonly Dictionary ParamAliases = new Dictionary(StringComparer.OrdinalIgnoreCase)
diff --git a/MCPForUnity/Editor/Tools/GetTestJob.cs b/MCPForUnity/Editor/Tools/GetTestJob.cs
index 4817ab9bc..ade8811e5 100644
--- a/MCPForUnity/Editor/Tools/GetTestJob.cs
+++ b/MCPForUnity/Editor/Tools/GetTestJob.cs
@@ -8,7 +8,7 @@ namespace MCPForUnity.Editor.Tools
///
/// Poll a previously started async test job by job_id.
///
- [McpForUnityTool("get_test_job", AutoRegister = false)]
+ [McpForUnityTool("get_test_job", AutoRegister = false, Group = "testing")]
public static class GetTestJob
{
public static object HandleCommand(JObject @params)
diff --git a/MCPForUnity/Editor/Tools/ManageScene.cs b/MCPForUnity/Editor/Tools/ManageScene.cs
index b07a34c3b..ec59533ae 100644
--- a/MCPForUnity/Editor/Tools/ManageScene.cs
+++ b/MCPForUnity/Editor/Tools/ManageScene.cs
@@ -240,6 +240,16 @@ public static object ExecuteScreenshot(string fileName = null, int? superSize =
return CaptureScreenshot(cmd);
}
+ ///
+ /// Captures a 6-angle contact-sheet around the scene bounds centre.
+ /// Public so the tools UI can reuse the same logic.
+ ///
+ public static object ExecuteMultiviewScreenshot(int maxResolution = 480)
+ {
+ var cmd = new SceneCommand { maxResolution = maxResolution };
+ return CaptureSurroundBatch(cmd);
+ }
+
private static object CreateScene(string fullPath, string relativePath)
{
if (File.Exists(fullPath))
diff --git a/MCPForUnity/Editor/Tools/ManageScriptableObject.cs b/MCPForUnity/Editor/Tools/ManageScriptableObject.cs
index 62d1c19a5..b5a4d78f3 100644
--- a/MCPForUnity/Editor/Tools/ManageScriptableObject.cs
+++ b/MCPForUnity/Editor/Tools/ManageScriptableObject.cs
@@ -17,7 +17,7 @@ namespace MCPForUnity.Editor.Tools
///
/// Patching is performed via SerializedObject/SerializedProperty paths (Unity-native), not reflection.
///
- [McpForUnityTool("manage_scriptable_object", AutoRegister = false)]
+ [McpForUnityTool("manage_scriptable_object", AutoRegister = false, Group = "scripting_ext")]
public static class ManageScriptableObject
{
private const string CodeCompilingOrReloading = "compiling_or_reloading";
diff --git a/MCPForUnity/Editor/Tools/ManageShader.cs b/MCPForUnity/Editor/Tools/ManageShader.cs
index 849a6f932..64cfbeb78 100644
--- a/MCPForUnity/Editor/Tools/ManageShader.cs
+++ b/MCPForUnity/Editor/Tools/ManageShader.cs
@@ -12,7 +12,7 @@ namespace MCPForUnity.Editor.Tools
///
/// Handles CRUD operations for shader files within the Unity project.
///
- [McpForUnityTool("manage_shader", AutoRegister = false)]
+ [McpForUnityTool("manage_shader", AutoRegister = false, Group = "vfx")]
public static class ManageShader
{
///
diff --git a/MCPForUnity/Editor/Tools/ManageTexture.cs b/MCPForUnity/Editor/Tools/ManageTexture.cs
index 86e429045..4ab66f720 100644
--- a/MCPForUnity/Editor/Tools/ManageTexture.cs
+++ b/MCPForUnity/Editor/Tools/ManageTexture.cs
@@ -13,7 +13,7 @@ namespace MCPForUnity.Editor.Tools
/// Supports patterns (checkerboard, stripes, dots, grid, brick),
/// gradients, noise, and direct pixel manipulation.
///
- [McpForUnityTool("manage_texture", AutoRegister = false)]
+ [McpForUnityTool("manage_texture", AutoRegister = false, Group = "vfx")]
public static class ManageTexture
{
private const int MaxTextureDimension = 1024;
diff --git a/MCPForUnity/Editor/Tools/ManageUI.cs b/MCPForUnity/Editor/Tools/ManageUI.cs
index c32253f4b..5947eb1f1 100644
--- a/MCPForUnity/Editor/Tools/ManageUI.cs
+++ b/MCPForUnity/Editor/Tools/ManageUI.cs
@@ -12,7 +12,7 @@
namespace MCPForUnity.Editor.Tools
{
- [McpForUnityTool("manage_ui", AutoRegister = false)]
+ [McpForUnityTool("manage_ui", AutoRegister = false, Group = "ui")]
public static class ManageUI
{
private static readonly HashSet ValidExtensions = new(StringComparer.OrdinalIgnoreCase)
diff --git a/MCPForUnity/Editor/Tools/McpForUnityToolAttribute.cs b/MCPForUnity/Editor/Tools/McpForUnityToolAttribute.cs
index e4db3a4c6..0d73fbb11 100644
--- a/MCPForUnity/Editor/Tools/McpForUnityToolAttribute.cs
+++ b/MCPForUnity/Editor/Tools/McpForUnityToolAttribute.cs
@@ -30,6 +30,15 @@ public class McpForUnityToolAttribute : Attribute
///
public bool AutoRegister { get; set; } = true;
+ ///
+ /// Tool group for dynamic visibility on the Python server.
+ /// Core tools are enabled by default; other groups start hidden and
+ /// can be activated per-session via the manage_tools meta-tool.
+ /// Valid groups: core, vfx, animation, ui, scripting_ext, testing, menu.
+ /// Set to null for server meta-tools that should always be visible.
+ ///
+ public string Group { get; set; } = "core";
+
///
/// Enables the polling middleware for long-running tools. When true, Unity
/// should return a PendingResponse and the Python side will poll using
diff --git a/MCPForUnity/Editor/Tools/RunTests.cs b/MCPForUnity/Editor/Tools/RunTests.cs
index abbf9225b..3c93e97d2 100644
--- a/MCPForUnity/Editor/Tools/RunTests.cs
+++ b/MCPForUnity/Editor/Tools/RunTests.cs
@@ -12,7 +12,7 @@ namespace MCPForUnity.Editor.Tools
/// Starts a Unity Test Runner run asynchronously and returns a job id immediately.
/// Use get_test_job(job_id) to poll status/results.
///
- [McpForUnityTool("run_tests", AutoRegister = false)]
+ [McpForUnityTool("run_tests", AutoRegister = false, Group = "testing")]
public static class RunTests
{
public static Task
- [McpForUnityTool("manage_vfx", AutoRegister = false)]
+ [McpForUnityTool("manage_vfx", AutoRegister = false, Group = "vfx")]
public static class ManageVFX
{
private static readonly Dictionary ParamAliases = new Dictionary(StringComparer.OrdinalIgnoreCase)
diff --git a/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs b/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
index df2aa1781..294a50f94 100644
--- a/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
+++ b/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
@@ -33,6 +33,10 @@ public class McpClientConfigSection
private VisualElement claudeCliPathRow;
private TextField claudeCliPath;
private Button browseClaudeButton;
+ private VisualElement clientProjectDirRow;
+ private TextField clientProjectDirField;
+ private Button browseProjectDirButton;
+ private Button clearProjectDirButton;
private Foldout manualConfigFoldout;
private TextField configPathField;
private Button copyPathButton;
@@ -84,6 +88,10 @@ private void CacheUIElements()
claudeCliPathRow = Root.Q("claude-cli-path-row");
claudeCliPath = Root.Q("claude-cli-path");
browseClaudeButton = Root.Q