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 HandleCommand(JObject @params) diff --git a/MCPForUnity/Editor/Tools/Vfx/ManageVFX.cs b/MCPForUnity/Editor/Tools/Vfx/ManageVFX.cs index 685bbf4cb..085e4069e 100644 --- a/MCPForUnity/Editor/Tools/Vfx/ManageVFX.cs +++ b/MCPForUnity/Editor/Tools/Vfx/ManageVFX.cs @@ -117,7 +117,7 @@ namespace MCPForUnity.Editor.Tools.Vfx /// /// For full parameter details, refer to Unity documentation for each component type. /// - [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