diff --git a/.gitignore b/.gitignore index b43d9f212..6bebce056 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ .codeiumignore .kiro +# Local worktrees for parallel feature work +.worktrees/ + # Code-copy related files .clipignore diff --git a/MCPForUnity/Editor/Clients/Configurators/ClaudeDesktopConfigurator.cs b/MCPForUnity/Editor/Clients/Configurators/ClaudeDesktopConfigurator.cs index ebce02d3c..9ecd057b5 100644 --- a/MCPForUnity/Editor/Clients/Configurators/ClaudeDesktopConfigurator.cs +++ b/MCPForUnity/Editor/Clients/Configurators/ClaudeDesktopConfigurator.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; using System.IO; -using MCPForUnity.Editor.Constants; using MCPForUnity.Editor.Models; -using MCPForUnity.Editor.Services; using UnityEditor; namespace MCPForUnity.Editor.Clients.Configurators @@ -39,27 +37,7 @@ public override string GetSkillInstallPath() "Save and restart Claude Desktop" }; - public override void Configure() - { - bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport; - if (useHttp) - { - throw new InvalidOperationException("Claude Desktop does not support HTTP transport. Switch to stdio in settings before configuring."); - } - - base.Configure(); - } - - public override string GetManualSnippet() - { - bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport; - if (useHttp) - { - return "# Claude Desktop does not support HTTP transport.\n" + - "# In Connect tab, change the Transport option from HTTP to stdio, then regenerate."; - } - - return base.GetManualSnippet(); - } + private static readonly ConfiguredTransport[] StdioOnly = { ConfiguredTransport.Stdio }; + public override IReadOnlyList SupportedTransports => StdioOnly; } } diff --git a/MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs b/MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs index 10fefff08..d103528df 100644 --- a/MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs +++ b/MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs @@ -26,6 +26,19 @@ public interface IMcpClientConfigurator /// True if this client supports auto-configure. bool SupportsAutoConfigure { get; } + /// + /// True if this client appears installed on the user's machine. Used to filter + /// "configure all detected" so we don't write configs for apps the user doesn't have. + /// Implementations should be cheap (filesystem stat or cached path lookup). + /// + bool IsInstalled { get; } + + /// + /// Transports this client can be configured with. Order is "preference if user has no opinion"; + /// the configure path picks the user's global preference if present in this list, else falls back to the first entry. + /// + System.Collections.Generic.IReadOnlyList SupportedTransports { get; } + /// Label to show on the configure button for the current state. string GetConfigureActionLabel(); diff --git a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs index 7b7f9b8cd..5c3d81c7c 100644 --- a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs +++ b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs @@ -30,6 +30,14 @@ protected McpClientConfiguratorBase(McpClient client) public McpStatus Status => client.status; public ConfiguredTransport ConfiguredTransport => client.configuredTransport; public virtual bool SupportsAutoConfigure => true; + // Default to a filesystem check on the configured path. Concrete configurators + // whose presence isn't path-based (CLI binaries, etc.) override this. This makes + // any future configurator that forgets to override fail-closed rather than be + // treated as "detected" by ConfigureAllDetectedClients. + public virtual bool IsInstalled => ParentDirectoryExists(GetConfigPath()); + private static readonly ConfiguredTransport[] DefaultTransports = + { ConfiguredTransport.Stdio, ConfiguredTransport.Http }; + public virtual IReadOnlyList SupportedTransports => DefaultTransports; public virtual bool SupportsSkills => false; public virtual string GetConfigureActionLabel() => "Configure"; public virtual string GetSkillInstallPath() => null; @@ -59,6 +67,17 @@ protected string CurrentOsPath() return client.linuxConfigPath; } + protected static bool ParentDirectoryExists(string configPath) + { + try + { + if (string.IsNullOrEmpty(configPath)) return false; + string parent = Path.GetDirectoryName(configPath); + return !string.IsNullOrEmpty(parent) && Directory.Exists(parent); + } + catch { return false; } + } + protected bool UrlsEqual(string a, string b) { if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b)) @@ -131,6 +150,8 @@ public JsonFileMcpConfigurator(McpClient client) : base(client) { } public override string GetConfigPath() => CurrentOsPath(); + public override bool IsInstalled => ParentDirectoryExists(GetConfigPath()); + public override McpStatus CheckStatus(bool attemptAutoRewrite = true) { try @@ -148,43 +169,37 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true) string configuredUrl = null; bool configExists = false; - if (client.IsVsCodeLayout) { - var vsConfig = JsonConvert.DeserializeObject(configJson) as JObject; - if (vsConfig != null) + var rootConfig = JsonConvert.DeserializeObject(configJson) as JObject; + JToken unityToken = null; + if (rootConfig != null) { - var unityToken = - vsConfig["servers"]?["unityMCP"] - ?? vsConfig["mcp"]?["servers"]?["unityMCP"]; + unityToken = client.IsVsCodeLayout + ? rootConfig["servers"]?["unityMCP"] + ?? rootConfig["mcp"]?["servers"]?["unityMCP"] + : rootConfig["mcpServers"]?["unityMCP"]; + } - if (unityToken is JObject unityObj) - { - configExists = true; + if (unityToken is JObject unityObj) + { + configExists = true; - var argsToken = unityObj["args"]; - if (argsToken is JArray) - { - args = argsToken.ToObject(); - } + var argsToken = unityObj["args"]; + if (argsToken is JArray) + { + args = argsToken.ToObject(); + } - var urlToken = unityObj["url"] ?? unityObj["serverUrl"]; - if (urlToken != null && urlToken.Type != JTokenType.Null) - { - configuredUrl = urlToken.ToString(); - } + // Clients diverge on the HTTP URL property name: "url" (Cursor/VSCode/Claude), + // "serverUrl" (Antigravity/Windsurf), "httpUrl" (Gemini CLI). Accept all three + // so CheckStatus matches what Configure() actually wrote. + var urlToken = unityObj["url"] ?? unityObj["serverUrl"] ?? unityObj["httpUrl"]; + if (urlToken != null && urlToken.Type != JTokenType.Null) + { + configuredUrl = urlToken.ToString(); } } } - else - { - McpConfig standardConfig = JsonConvert.DeserializeObject(configJson); - if (standardConfig?.mcpServers?.unityMCP != null) - { - args = standardConfig.mcpServers.unityMCP.args; - configuredUrl = standardConfig.mcpServers.unityMCP.url; - configExists = true; - } - } if (!configExists) { @@ -357,6 +372,8 @@ public CodexMcpConfigurator(McpClient client) : base(client) { } public override string GetConfigPath() => CurrentOsPath(); + public override bool IsInstalled => ParentDirectoryExists(GetConfigPath()); + public override McpStatus CheckStatus(bool attemptAutoRewrite = true) { try @@ -542,6 +559,8 @@ public ClaudeCliMcpConfigurator(McpClient client) : base(client) { } public override string GetConfigPath() => "Managed via Claude CLI"; + public override bool IsInstalled => MCPServiceLocator.Paths.IsClaudeCliDetected(); + /// /// 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 diff --git a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs index d8aa8e3f5..3d99cc856 100644 --- a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs +++ b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs @@ -49,6 +49,7 @@ internal static class EditorPrefKeys internal const string ResourceFoldoutStatePrefix = "MCPForUnity.ResourceFoldout."; internal const string EditorWindowActivePanel = "MCPForUnity.EditorWindow.ActivePanel"; internal const string LastSelectedClientId = "MCPForUnity.LastSelectedClientId"; + internal const string ClientDetailsFoldoutOpen = "MCPForUnity.ClientConfig.DetailsFoldoutOpen"; internal const string SetupCompleted = "MCPForUnity.SetupCompleted"; internal const string SetupDismissed = "MCPForUnity.SetupDismissed"; diff --git a/MCPForUnity/Editor/MCPForUnity.Editor.asmdef b/MCPForUnity/Editor/MCPForUnity.Editor.asmdef index 3021ad316..e8f0ecd6d 100644 --- a/MCPForUnity/Editor/MCPForUnity.Editor.asmdef +++ b/MCPForUnity/Editor/MCPForUnity.Editor.asmdef @@ -14,6 +14,12 @@ ], "autoReferenced": true, "defineConstraints": [], - "versionDefines": [], + "versionDefines": [ + { + "name": "com.unity.visualeffectgraph", + "expression": "0.0.0", + "define": "UNITY_VFX_GRAPH" + } + ], "noEngineReferences": false } \ No newline at end of file diff --git a/MCPForUnity/Editor/Services/ClientConfigurationService.cs b/MCPForUnity/Editor/Services/ClientConfigurationService.cs index 65f0e1d33..d3eb86693 100644 --- a/MCPForUnity/Editor/Services/ClientConfigurationService.cs +++ b/MCPForUnity/Editor/Services/ClientConfigurationService.cs @@ -30,7 +30,7 @@ public void ConfigureClient(IMcpClientConfigurator configurator) AssetPathUtility.CleanLocalServerBuildArtifacts(); } - configurator.Configure(); + ConfigureWithTransportCoercion(configurator); } public ClientConfigurationSummary ConfigureAllDetectedClients() @@ -44,11 +44,16 @@ public ClientConfigurationSummary ConfigureAllDetectedClients() var summary = new ClientConfigurationSummary(); foreach (var configurator in configurators) { + if (!configurator.IsInstalled) + { + summary.SkippedCount++; + continue; + } try { // Always re-run configuration so core fields stay current configurator.CheckStatus(attemptAutoRewrite: false); - configurator.Configure(); + ConfigureWithTransportCoercion(configurator); summary.SuccessCount++; summary.Messages.Add($"✓ {configurator.DisplayName}: Configured successfully"); } @@ -62,6 +67,20 @@ public ClientConfigurationSummary ConfigureAllDetectedClients() return summary; } + private static void ConfigureWithTransportCoercion(IMcpClientConfigurator configurator) + { + bool originalHttp = EditorConfigurationCache.Instance.UseHttpTransport; + try + { + CoerceTransportFor(configurator); + configurator.Configure(); + } + finally + { + EditorConfigurationCache.Instance.SetUseHttpTransport(originalHttp); + } + } + public bool CheckClientStatus(IMcpClientConfigurator configurator, bool attemptAutoRewrite = true) { var previous = configurator.Status; @@ -69,5 +88,59 @@ public bool CheckClientStatus(IMcpClientConfigurator configurator, bool attemptA return current != previous; } + private static void CoerceTransportFor(IMcpClientConfigurator configurator) + { + var supported = configurator.SupportedTransports; + if (supported == null || supported.Count == 0) return; + + bool currentlyHttp = EditorConfigurationCache.Instance.UseHttpTransport; + var requested = currentlyHttp ? ConfiguredTransport.Http : ConfiguredTransport.Stdio; + + // Accept any HTTP variant (Http, HttpRemote) when the user wants HTTP — a client that + // only supports HttpRemote should not get coerced to stdio just because Http isn't + // explicitly listed. + if (SupportsRequested(supported, requested)) return; + + // Fall back in the direction of the user's intent: if they wanted HTTP, prefer any + // HTTP variant the client does support; otherwise prefer stdio. Honors the + // configurator's declared order when more than one option remains. + ConfiguredTransport chosen = PickFallback(supported, requested); + bool needHttp = IsHttpVariant(chosen); + if (EditorConfigurationCache.Instance.UseHttpTransport != needHttp) + { + EditorConfigurationCache.Instance.SetUseHttpTransport(needHttp); + McpLog.Info( + $"[{configurator.DisplayName}] auto-selected {chosen} transport (client does not support {requested})."); + } + } + + private static bool IsHttpVariant(ConfiguredTransport t) + => t == ConfiguredTransport.Http || t == ConfiguredTransport.HttpRemote; + + private static bool SupportsRequested(IReadOnlyList supported, ConfiguredTransport requested) + { + if (requested == ConfiguredTransport.Http) + { + foreach (var t in supported) + if (IsHttpVariant(t)) return true; + return false; + } + return supported.Contains(requested); + } + + private static ConfiguredTransport PickFallback(IReadOnlyList supported, ConfiguredTransport requested) + { + if (requested == ConfiguredTransport.Http) + { + foreach (var t in supported) + if (IsHttpVariant(t)) return t; + } + else + { + foreach (var t in supported) + if (t == ConfiguredTransport.Stdio) return t; + } + return supported[0]; + } } } diff --git a/MCPForUnity/Editor/Services/StartupConfigRewrite.cs b/MCPForUnity/Editor/Services/StartupConfigRewrite.cs new file mode 100644 index 000000000..2df69f3a3 --- /dev/null +++ b/MCPForUnity/Editor/Services/StartupConfigRewrite.cs @@ -0,0 +1,96 @@ +using System; +using System.Reflection; +using MCPForUnity.Editor.Clients; +using MCPForUnity.Editor.Constants; +using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Models; +using UnityEditor; + +namespace MCPForUnity.Editor.Services +{ + /// + /// Once per Editor session, sweeps registered configurators and re-runs CheckStatus(attemptAutoRewrite: true) + /// for any installed client that already has a config on disk. Catches the case where the user updated the + /// MCP for Unity package while the Editor was closed — without this sweep, stale package versions in client + /// configs would persist until the user opens the MCP window. + /// + [InitializeOnLoad] + public static class StartupConfigRewrite + { + public const string SESSION_GUARD_KEY = "MCPForUnity.StartupConfigRewrite.Ran"; + + static StartupConfigRewrite() + { + if (UnityEditorInternal.InternalEditorUtility.inBatchMode) return; + // AssetImportWorker subprocesses share [InitializeOnLoad] but don't host MCP and + // shouldn't be writing client configs from a half-loaded domain (same surface as + // issue #1134 in CommandRegistry). + if (IsRunningInAssetImportWorker()) return; + if (SessionState.GetBool(SESSION_GUARD_KEY, false)) return; + EditorApplication.delayCall += RunOnce; + } + + private static void RunOnce() + { + if (SessionState.GetBool(SESSION_GUARD_KEY, false)) return; + SessionState.SetBool(SESSION_GUARD_KEY, true); + + if (!EditorPrefs.GetBool(EditorPrefKeys.AutoRegisterEnabled, true)) return; + + int rewrote = 0; + foreach (var c in McpClientRegistry.All) + { + try + { + if (!c.IsInstalled) continue; + // Always let CheckStatus read the current state from disk before deciding — + // the in-memory Status can be NotConfigured on a fresh editor load even + // though the file already has a valid config. + var before = c.Status; + var after = c.CheckStatus(attemptAutoRewrite: true); + if (before != after && after == McpStatus.Configured) rewrote++; + } + catch (System.Exception ex) + { + McpLog.Warn($"[StartupConfigRewrite] {c.DisplayName} failed: {ex.Message}"); + } + } + if (rewrote > 0) + McpLog.Info($"[StartupConfigRewrite] refreshed {rewrote} client config(s)."); + } + + private static bool? _cachedIsAssetImportWorker; + + private static bool IsRunningInAssetImportWorker() + { + if (_cachedIsAssetImportWorker.HasValue) + return _cachedIsAssetImportWorker.Value; + + bool result = false; + try + { + var method = typeof(UnityEditor.AssetDatabase).GetMethod( + "IsAssetImportWorkerProcess", + BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + if (method != null && method.GetParameters().Length == 0) + result = method.Invoke(null, null) is bool b && b; + } + catch { } + + if (!result) + { + try + { + string cmd = Environment.CommandLine ?? string.Empty; + if (cmd.IndexOf("-importWorker", StringComparison.OrdinalIgnoreCase) >= 0 + || cmd.IndexOf("AssetImportWorker", StringComparison.OrdinalIgnoreCase) >= 0) + result = true; + } + catch { } + } + + _cachedIsAssetImportWorker = result; + return result; + } + } +} diff --git a/MCPForUnity/Editor/Services/StartupConfigRewrite.cs.meta b/MCPForUnity/Editor/Services/StartupConfigRewrite.cs.meta new file mode 100644 index 000000000..561854a71 --- /dev/null +++ b/MCPForUnity/Editor/Services/StartupConfigRewrite.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 545839736dec4fc49c7392412dac7856 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs b/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs index 245f12253..66721f7b9 100644 --- a/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs +++ b/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs @@ -481,7 +481,7 @@ private static async Task HandleClientAsync(TcpClient client, CancellationToken try { var ep = client.Client?.RemoteEndPoint?.ToString() ?? "unknown"; - McpLog.Info($"Client connected {ep} (active clients: {clientCount})"); + McpLog.Info($"Client connected {ep} (active clients: {clientCount})", always: false); } catch { } try @@ -517,7 +517,7 @@ private static async Task HandleClientAsync(TcpClient client, CancellationToken } if (staleClients.Length > 0) { - McpLog.Info($"Closing {staleClients.Length} stale client(s) after new connection"); + McpLog.Info($"Closing {staleClients.Length} stale client(s) after new connection", always: false); foreach (var stale in staleClients) { try { stale.Close(); } catch { } @@ -649,7 +649,7 @@ private static async Task HandleClientAsync(TcpClient client, CancellationToken lock (clientsLock) { activeClients.Remove(client); } int remaining; lock (clientsLock) { remaining = activeClients.Count; } - McpLog.Info($"Client handler exited (remaining clients: {remaining})"); + McpLog.Info($"Client handler exited (remaining clients: {remaining})", always: false); } } } diff --git a/MCPForUnity/Editor/Tools/CommandRegistry.cs b/MCPForUnity/Editor/Tools/CommandRegistry.cs index c22ab00bb..7588e2cfb 100644 --- a/MCPForUnity/Editor/Tools/CommandRegistry.cs +++ b/MCPForUnity/Editor/Tools/CommandRegistry.cs @@ -58,6 +58,16 @@ public static void Initialize() /// private static void AutoDiscoverCommands() { + // AssetImportWorker is a separate Editor subprocess. It doesn't host the MCP + // transport so the registry is unused there, and Mono can hard-crash inside + // GetCustomAttribute() when scanning types whose owning assembly hasn't + // finished domain-reload bookkeeping in the worker. Skip the scan there + // entirely. See issue #1134. + if (IsRunningInAssetImportWorker()) + { + return; + } + try { var allTypes = UnityAssembliesCompat.GetLoadedAssemblies() @@ -70,7 +80,7 @@ private static void AutoDiscoverCommands() .ToList(); // Discover tools - var toolTypes = allTypes.Where(t => t.GetCustomAttribute() != null); + var toolTypes = allTypes.Where(t => HasAttributeSafe(t)); int toolCount = 0; foreach (var type in toolTypes) { @@ -79,7 +89,7 @@ private static void AutoDiscoverCommands() } // Discover resources - var resourceTypes = allTypes.Where(t => t.GetCustomAttribute() != null); + var resourceTypes = allTypes.Where(t => HasAttributeSafe(t)); int resourceCount = 0; foreach (var type in resourceTypes) { @@ -95,6 +105,64 @@ private static void AutoDiscoverCommands() } } + private static bool HasAttributeSafe(Type type) where T : Attribute + { + try + { + return type.GetCustomAttribute() != null; + } + catch + { + // Type metadata can be in a half-loaded state during domain reload; treat + // those as "no attribute" rather than aborting the whole scan. + return false; + } + } + + private static bool? _cachedIsAssetImportWorker; + + private static bool IsRunningInAssetImportWorker() + { + if (_cachedIsAssetImportWorker.HasValue) + return _cachedIsAssetImportWorker.Value; + + bool result = false; + try + { + // AssetDatabase.IsAssetImportWorkerProcess() exists on Unity 2020.2+ but the + // visibility has shifted between versions. Look it up reflectively so we + // tolerate either signature without conditional compilation. + var method = typeof(UnityEditor.AssetDatabase).GetMethod( + "IsAssetImportWorkerProcess", + BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + if (method != null && method.GetParameters().Length == 0) + { + result = method.Invoke(null, null) is bool b && b; + } + } + catch + { + // Reflection problems shouldn't break startup; fall through to the cmdline check. + } + + if (!result) + { + try + { + string cmd = Environment.CommandLine ?? string.Empty; + if (cmd.IndexOf("-importWorker", StringComparison.OrdinalIgnoreCase) >= 0 + || cmd.IndexOf("AssetImportWorker", StringComparison.OrdinalIgnoreCase) >= 0) + { + result = true; + } + } + catch { } + } + + _cachedIsAssetImportWorker = result; + return result; + } + /// /// Register a command type (tool or resource) with the registry. /// Returns true if successfully registered, false otherwise. diff --git a/MCPForUnity/Editor/Tools/Vfx/VfxGraphAssets.cs b/MCPForUnity/Editor/Tools/Vfx/VfxGraphAssets.cs index 5f2e575f6..68a95da05 100644 --- a/MCPForUnity/Editor/Tools/Vfx/VfxGraphAssets.cs +++ b/MCPForUnity/Editor/Tools/Vfx/VfxGraphAssets.cs @@ -39,8 +39,6 @@ public static object ListAssets(JObject @params) return new { success = false, message = "VFX Graph package (com.unity.visualeffectgraph) not installed" }; } #else - private static readonly string[] SupportedVfxGraphVersions = { "12.1" }; - /// /// Creates a new VFX Graph asset file from a template. /// @@ -486,54 +484,17 @@ public static object ListAssets(JObject @params) private static string ValidateVfxGraphVersion() { + // UNITY_VFX_GRAPH is set by the asmdef versionDefines whenever the package is + // installed, so reaching this branch already implies presence. Keep a runtime + // double-check for the rare window during install/uninstall where the compile + // gate and the package state can briefly disagree. var info = UnityEditor.PackageManager.PackageInfo.FindForAssetPath("Packages/com.unity.visualeffectgraph"); if (info == null) { return "VFX Graph package (com.unity.visualeffectgraph) not installed"; } - if (IsVersionSupported(info.version)) - { - return null; - } - - string supported = string.Join(", ", SupportedVfxGraphVersions.Select(version => $"{version}.x")); - return $"Unsupported VFX Graph version {info.version}. Supported versions: {supported}."; - } - - private static bool IsVersionSupported(string installedVersion) - { - if (string.IsNullOrEmpty(installedVersion)) - { - return false; - } - - string normalized = installedVersion; - int suffixIndex = normalized.IndexOfAny(new[] { '-', '+' }); - if (suffixIndex >= 0) - { - normalized = normalized.Substring(0, suffixIndex); - } - - if (!Version.TryParse(normalized, out Version installed)) - { - return false; - } - - foreach (string supported in SupportedVfxGraphVersions) - { - if (!Version.TryParse(supported, out Version target)) - { - continue; - } - - if (installed.Major == target.Major && installed.Minor == target.Minor) - { - return true; - } - } - - return false; + return null; } private static string TryGetAssetPathFromFileSystem(string templatePath) diff --git a/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs b/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs index 294a50f94..33b63421b 100644 --- a/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs +++ b/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs @@ -37,6 +37,7 @@ public class McpClientConfigSection private TextField clientProjectDirField; private Button browseProjectDirButton; private Button clearProjectDirButton; + private Foldout clientDetailsFoldout; private Foldout manualConfigFoldout; private TextField configPathField; private Button copyPathButton; @@ -92,6 +93,7 @@ private void CacheUIElements() clientProjectDirField = Root.Q("client-project-dir"); browseProjectDirButton = Root.Q