From d9337701c193f988d43072f791bdd7ec5ed376aa Mon Sep 17 00:00:00 2001 From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com> Date: Mon, 18 May 2026 16:01:00 +0800 Subject: [PATCH 01/22] chore: ignore .worktrees/ for local parallel feature work --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 1053b2ad0..f69375802 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ .codeiumignore .kiro +# Local worktrees for parallel feature work +.worktrees/ + # Code-copy related files .clipignore From 69be612a3516d3fcd3d89cfc135b18c2bf94691e Mon Sep 17 00:00:00 2001 From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com> Date: Mon, 18 May 2026 16:05:47 +0800 Subject: [PATCH 02/22] feat(clients): add IsInstalled detection to configurator contract --- .../Editor/Clients/IMcpClientConfigurator.cs | 7 +++ .../Clients/McpClientConfiguratorBase.cs | 43 ++++++++++++++++++ .../Assets/Tests/EditMode/Clients.meta | 8 ++++ .../EditMode/Clients/IsInstalledTests.cs | 45 +++++++++++++++++++ .../EditMode/Clients/IsInstalledTests.cs.meta | 11 +++++ 5 files changed, 114 insertions(+) create mode 100644 TestProjects/UnityMCPTests/Assets/Tests/EditMode/Clients.meta create mode 100644 TestProjects/UnityMCPTests/Assets/Tests/EditMode/Clients/IsInstalledTests.cs create mode 100644 TestProjects/UnityMCPTests/Assets/Tests/EditMode/Clients/IsInstalledTests.cs.meta diff --git a/MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs b/MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs index 10fefff08..99b9771aa 100644 --- a/MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs +++ b/MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs @@ -26,6 +26,13 @@ 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; } + /// 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..389347a6e 100644 --- a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs +++ b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs @@ -30,6 +30,7 @@ protected McpClientConfiguratorBase(McpClient client) public McpStatus Status => client.status; public ConfiguredTransport ConfiguredTransport => client.configuredTransport; public virtual bool SupportsAutoConfigure => true; + public virtual bool IsInstalled => true; public virtual bool SupportsSkills => false; public virtual string GetConfigureActionLabel() => "Configure"; public virtual string GetSkillInstallPath() => null; @@ -131,6 +132,21 @@ public JsonFileMcpConfigurator(McpClient client) : base(client) { } public override string GetConfigPath() => CurrentOsPath(); + public override bool IsInstalled + { + get + { + try + { + string path = GetConfigPath(); + if (string.IsNullOrEmpty(path)) return false; + string parent = Path.GetDirectoryName(path); + return !string.IsNullOrEmpty(parent) && Directory.Exists(parent); + } + catch { return false; } + } + } + public override McpStatus CheckStatus(bool attemptAutoRewrite = true) { try @@ -357,6 +373,21 @@ public CodexMcpConfigurator(McpClient client) : base(client) { } public override string GetConfigPath() => CurrentOsPath(); + public override bool IsInstalled + { + get + { + try + { + string path = GetConfigPath(); + if (string.IsNullOrEmpty(path)) return false; + string parent = Path.GetDirectoryName(path); + return !string.IsNullOrEmpty(parent) && Directory.Exists(parent); + } + catch { return false; } + } + } + public override McpStatus CheckStatus(bool attemptAutoRewrite = true) { try @@ -542,6 +573,18 @@ public ClaudeCliMcpConfigurator(McpClient client) : base(client) { } public override string GetConfigPath() => "Managed via Claude CLI"; + public override bool IsInstalled + { + get + { + try + { + return !string.IsNullOrEmpty(MCPServiceLocator.Paths.GetClaudeCliPath()); + } + catch { return false; } + } + } + /// /// 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/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Clients.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Clients.meta new file mode 100644 index 000000000..3421dd49a --- /dev/null +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Clients.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 57176dc46eea4bbba356c3959cfeb33f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Clients/IsInstalledTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Clients/IsInstalledTests.cs new file mode 100644 index 000000000..e7d4467cb --- /dev/null +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Clients/IsInstalledTests.cs @@ -0,0 +1,45 @@ +using System.IO; +using MCPForUnity.Editor.Clients; +using MCPForUnity.Editor.Clients.Configurators; +using NUnit.Framework; + +namespace MCPForUnityTests.Editor.Clients +{ + [TestFixture] + public class IsInstalledTests + { + [Test] + public void IMcpClientConfigurator_ExposesIsInstalled() + { + var prop = typeof(IMcpClientConfigurator).GetProperty("IsInstalled"); + Assert.IsNotNull(prop, "IMcpClientConfigurator must expose an IsInstalled property"); + Assert.AreEqual(typeof(bool), prop.PropertyType); + } + + [Test] + public void JsonClient_NotInstalled_WhenParentDirMissing() + { + var cursor = new CursorConfigurator(); + string parent = Path.GetDirectoryName(cursor.GetConfigPath()); + if (parent == null || !Directory.Exists(parent)) + { + Assert.IsFalse(cursor.IsInstalled, + "Cursor parent dir does not exist on this machine, IsInstalled must be false"); + } + else + { + Assert.IsTrue(cursor.IsInstalled, + "Cursor parent dir exists, IsInstalled must be true"); + } + } + + [Test] + public void JsonClient_Installed_WhenParentDirExists() + { + var claude = new ClaudeDesktopConfigurator(); + string parent = Path.GetDirectoryName(claude.GetConfigPath()); + bool expected = parent != null && Directory.Exists(parent); + Assert.AreEqual(expected, claude.IsInstalled); + } + } +} diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Clients/IsInstalledTests.cs.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Clients/IsInstalledTests.cs.meta new file mode 100644 index 000000000..155de700f --- /dev/null +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Clients/IsInstalledTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 15e00d05358545a5955aa1c291653289 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From fe43e61ad24ebf9a638a6208b140fdca4d56edee Mon Sep 17 00:00:00 2001 From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com> Date: Mon, 18 May 2026 16:12:17 +0800 Subject: [PATCH 03/22] refactor(clients): DRY IsInstalled overrides using shared helpers --- .../Clients/McpClientConfiguratorBase.cs | 53 +++++-------------- 1 file changed, 14 insertions(+), 39 deletions(-) diff --git a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs index 389347a6e..4990da709 100644 --- a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs +++ b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs @@ -60,6 +60,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)) @@ -132,20 +143,7 @@ public JsonFileMcpConfigurator(McpClient client) : base(client) { } public override string GetConfigPath() => CurrentOsPath(); - public override bool IsInstalled - { - get - { - try - { - string path = GetConfigPath(); - if (string.IsNullOrEmpty(path)) return false; - string parent = Path.GetDirectoryName(path); - return !string.IsNullOrEmpty(parent) && Directory.Exists(parent); - } - catch { return false; } - } - } + public override bool IsInstalled => ParentDirectoryExists(GetConfigPath()); public override McpStatus CheckStatus(bool attemptAutoRewrite = true) { @@ -373,20 +371,7 @@ public CodexMcpConfigurator(McpClient client) : base(client) { } public override string GetConfigPath() => CurrentOsPath(); - public override bool IsInstalled - { - get - { - try - { - string path = GetConfigPath(); - if (string.IsNullOrEmpty(path)) return false; - string parent = Path.GetDirectoryName(path); - return !string.IsNullOrEmpty(parent) && Directory.Exists(parent); - } - catch { return false; } - } - } + public override bool IsInstalled => ParentDirectoryExists(GetConfigPath()); public override McpStatus CheckStatus(bool attemptAutoRewrite = true) { @@ -573,17 +558,7 @@ public ClaudeCliMcpConfigurator(McpClient client) : base(client) { } public override string GetConfigPath() => "Managed via Claude CLI"; - public override bool IsInstalled - { - get - { - try - { - return !string.IsNullOrEmpty(MCPServiceLocator.Paths.GetClaudeCliPath()); - } - catch { return false; } - } - } + public override bool IsInstalled => MCPServiceLocator.Paths.IsClaudeCliDetected(); /// /// Returns the project directory that CLI-based configurators will use as the working directory From 6b064a131b8799a92330d6b18af63809b42763ce Mon Sep 17 00:00:00 2001 From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com> Date: Mon, 18 May 2026 16:20:07 +0800 Subject: [PATCH 04/22] feat(clients): per-client SupportedTransports with auto-coercion --- .../ClaudeDesktopConfigurator.cs | 12 ++----- .../Editor/Clients/IMcpClientConfigurator.cs | 6 ++++ .../Clients/McpClientConfiguratorBase.cs | 3 ++ .../Services/ClientConfigurationService.cs | 30 +++++++++++++++- .../Clients/SupportedTransportsTests.cs | 36 +++++++++++++++++++ .../Clients/SupportedTransportsTests.cs.meta | 11 ++++++ 6 files changed, 87 insertions(+), 11 deletions(-) create mode 100644 TestProjects/UnityMCPTests/Assets/Tests/EditMode/Clients/SupportedTransportsTests.cs create mode 100644 TestProjects/UnityMCPTests/Assets/Tests/EditMode/Clients/SupportedTransportsTests.cs.meta diff --git a/MCPForUnity/Editor/Clients/Configurators/ClaudeDesktopConfigurator.cs b/MCPForUnity/Editor/Clients/Configurators/ClaudeDesktopConfigurator.cs index ebce02d3c..9634853af 100644 --- a/MCPForUnity/Editor/Clients/Configurators/ClaudeDesktopConfigurator.cs +++ b/MCPForUnity/Editor/Clients/Configurators/ClaudeDesktopConfigurator.cs @@ -39,16 +39,8 @@ 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(); - } + private static readonly ConfiguredTransport[] StdioOnly = { ConfiguredTransport.Stdio }; + public override IReadOnlyList SupportedTransports => StdioOnly; public override string GetManualSnippet() { diff --git a/MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs b/MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs index 99b9771aa..d103528df 100644 --- a/MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs +++ b/MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs @@ -33,6 +33,12 @@ public interface IMcpClientConfigurator /// 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 4990da709..61d8cbc43 100644 --- a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs +++ b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs @@ -31,6 +31,9 @@ protected McpClientConfiguratorBase(McpClient client) public ConfiguredTransport ConfiguredTransport => client.configuredTransport; public virtual bool SupportsAutoConfigure => true; public virtual bool IsInstalled => true; + 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; diff --git a/MCPForUnity/Editor/Services/ClientConfigurationService.cs b/MCPForUnity/Editor/Services/ClientConfigurationService.cs index 65f0e1d33..8f3241739 100644 --- a/MCPForUnity/Editor/Services/ClientConfigurationService.cs +++ b/MCPForUnity/Editor/Services/ClientConfigurationService.cs @@ -30,7 +30,16 @@ public void ConfigureClient(IMcpClientConfigurator configurator) AssetPathUtility.CleanLocalServerBuildArtifacts(); } - configurator.Configure(); + bool originalHttp = EditorConfigurationCache.Instance.UseHttpTransport; + try + { + CoerceTransportFor(configurator); + configurator.Configure(); + } + finally + { + EditorConfigurationCache.Instance.SetUseHttpTransport(originalHttp); + } } public ClientConfigurationSummary ConfigureAllDetectedClients() @@ -69,5 +78,24 @@ 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; + + if (supported.Contains(requested)) return; // user preference is supported, no change + + var chosen = supported[0]; + bool needHttp = chosen == ConfiguredTransport.Http; + if (EditorConfigurationCache.Instance.UseHttpTransport != needHttp) + { + EditorConfigurationCache.Instance.SetUseHttpTransport(needHttp); + McpLog.Info( + $"[{configurator.DisplayName}] auto-selected {chosen} transport (client does not support {requested})."); + } + } } } diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Clients/SupportedTransportsTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Clients/SupportedTransportsTests.cs new file mode 100644 index 000000000..865561f0f --- /dev/null +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Clients/SupportedTransportsTests.cs @@ -0,0 +1,36 @@ +using System.Linq; +using MCPForUnity.Editor.Clients; +using MCPForUnity.Editor.Clients.Configurators; +using MCPForUnity.Editor.Models; +using NUnit.Framework; + +namespace MCPForUnityTests.Editor.Clients +{ + [TestFixture] + public class SupportedTransportsTests + { + [Test] + public void IMcpClientConfigurator_ExposesSupportedTransports() + { + var prop = typeof(IMcpClientConfigurator).GetProperty("SupportedTransports"); + Assert.IsNotNull(prop, "Must expose SupportedTransports"); + } + + [Test] + public void ClaudeDesktop_SupportsStdioOnly() + { + var claude = new ClaudeDesktopConfigurator(); + CollectionAssert.Contains(claude.SupportedTransports.ToList(), ConfiguredTransport.Stdio); + CollectionAssert.DoesNotContain(claude.SupportedTransports.ToList(), ConfiguredTransport.Http); + } + + [Test] + public void Cursor_SupportsBothTransports() + { + var cursor = new CursorConfigurator(); + var list = cursor.SupportedTransports.ToList(); + CollectionAssert.Contains(list, ConfiguredTransport.Stdio); + CollectionAssert.Contains(list, ConfiguredTransport.Http); + } + } +} diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Clients/SupportedTransportsTests.cs.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Clients/SupportedTransportsTests.cs.meta new file mode 100644 index 000000000..68bdc87ac --- /dev/null +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Clients/SupportedTransportsTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 26823824aa5cae0caa9488e9ad49bcf1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From 17dbf427e7a09e5c3fea80659cd44c9071300950 Mon Sep 17 00:00:00 2001 From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com> Date: Mon, 18 May 2026 16:27:24 +0800 Subject: [PATCH 05/22] fix(clients): apply transport coercion in bulk configure path --- .../ClaudeDesktopConfigurator.cs | 14 ---------- .../Services/ClientConfigurationService.cs | 27 +++++++++++-------- 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/MCPForUnity/Editor/Clients/Configurators/ClaudeDesktopConfigurator.cs b/MCPForUnity/Editor/Clients/Configurators/ClaudeDesktopConfigurator.cs index 9634853af..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 @@ -41,17 +39,5 @@ public override string GetSkillInstallPath() private static readonly ConfiguredTransport[] StdioOnly = { ConfiguredTransport.Stdio }; public override IReadOnlyList SupportedTransports => StdioOnly; - - 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(); - } } } diff --git a/MCPForUnity/Editor/Services/ClientConfigurationService.cs b/MCPForUnity/Editor/Services/ClientConfigurationService.cs index 8f3241739..b4a1fbad4 100644 --- a/MCPForUnity/Editor/Services/ClientConfigurationService.cs +++ b/MCPForUnity/Editor/Services/ClientConfigurationService.cs @@ -30,16 +30,7 @@ public void ConfigureClient(IMcpClientConfigurator configurator) AssetPathUtility.CleanLocalServerBuildArtifacts(); } - bool originalHttp = EditorConfigurationCache.Instance.UseHttpTransport; - try - { - CoerceTransportFor(configurator); - configurator.Configure(); - } - finally - { - EditorConfigurationCache.Instance.SetUseHttpTransport(originalHttp); - } + ConfigureWithTransportCoercion(configurator); } public ClientConfigurationSummary ConfigureAllDetectedClients() @@ -57,7 +48,7 @@ public ClientConfigurationSummary ConfigureAllDetectedClients() { // 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"); } @@ -71,6 +62,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; From 03daf97000a777bd03a086bd365ce642dfb84fe2 Mon Sep 17 00:00:00 2001 From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com> Date: Mon, 18 May 2026 16:31:38 +0800 Subject: [PATCH 06/22] feat(clients): filter ConfigureAll to only detected clients --- .../Services/ClientConfigurationService.cs | 5 ++++ .../ClientConfig/McpClientConfigSection.cs | 7 +++-- .../Services/ConfigureDetectedClientsTests.cs | 29 +++++++++++++++++++ .../ConfigureDetectedClientsTests.cs.meta | 11 +++++++ 4 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ConfigureDetectedClientsTests.cs create mode 100644 TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ConfigureDetectedClientsTests.cs.meta diff --git a/MCPForUnity/Editor/Services/ClientConfigurationService.cs b/MCPForUnity/Editor/Services/ClientConfigurationService.cs index b4a1fbad4..45de0fbe6 100644 --- a/MCPForUnity/Editor/Services/ClientConfigurationService.cs +++ b/MCPForUnity/Editor/Services/ClientConfigurationService.cs @@ -44,6 +44,11 @@ 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 diff --git a/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs b/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs index 294a50f94..b3f7b7386 100644 --- a/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs +++ b/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs @@ -284,13 +284,16 @@ private void OnConfigureAllClientsClicked() { var summary = MCPServiceLocator.Client.ConfigureAllDetectedClients(); - string message = summary.GetSummaryMessage() + "\n\n"; + string headline = summary.SkippedCount > 0 + ? $"{summary.SuccessCount + summary.FailureCount} detected client(s) processed. ({summary.SkippedCount} not installed, skipped.)" + : summary.GetSummaryMessage(); + string message = headline + "\n\n"; foreach (var msg in summary.Messages) { message += msg + "\n"; } - EditorUtility.DisplayDialog("Configure All Clients", message, "OK"); + EditorUtility.DisplayDialog("Configure Detected Clients", message, "OK"); if (selectedClientIndex >= 0 && selectedClientIndex < configurators.Count) { diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ConfigureDetectedClientsTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ConfigureDetectedClientsTests.cs new file mode 100644 index 000000000..a0ef9d111 --- /dev/null +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ConfigureDetectedClientsTests.cs @@ -0,0 +1,29 @@ +using System.Linq; +using MCPForUnity.Editor.Services; +using NUnit.Framework; + +namespace MCPForUnityTests.Editor.Services +{ + [TestFixture] + public class ConfigureDetectedClientsTests + { + [Test] + public void Summary_ContainsOnlyInstalledClients() + { + var svc = new ClientConfigurationService(); + var summary = svc.ConfigureAllDetectedClients(); + int installedCount = svc.GetAllClients().Count(c => c.IsInstalled); + Assert.AreEqual(installedCount, summary.SuccessCount + summary.FailureCount, + "Only installed clients should appear in success/failure totals"); + } + + [Test] + public void Summary_SkippedCountTracksUninstalled() + { + var svc = new ClientConfigurationService(); + var summary = svc.ConfigureAllDetectedClients(); + int uninstalledCount = svc.GetAllClients().Count(c => !c.IsInstalled); + Assert.AreEqual(uninstalledCount, summary.SkippedCount); + } + } +} diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ConfigureDetectedClientsTests.cs.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ConfigureDetectedClientsTests.cs.meta new file mode 100644 index 000000000..7fbbcf589 --- /dev/null +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ConfigureDetectedClientsTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 78f1385217c246d4a9b5c596986787b0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From 1bde9aab8635ff16ff7a9f866220a941a2fb8fd9 Mon Sep 17 00:00:00 2001 From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com> Date: Mon, 18 May 2026 16:35:30 +0800 Subject: [PATCH 07/22] feat(clients): startup auto-rewrite of stale client configs --- .../Editor/Services/StartupConfigRewrite.cs | 54 +++++++++++++++++++ .../Services/StartupConfigRewrite.cs.meta | 11 ++++ .../Services/StartupConfigRewriteTests.cs | 37 +++++++++++++ .../StartupConfigRewriteTests.cs.meta | 11 ++++ 4 files changed, 113 insertions(+) create mode 100644 MCPForUnity/Editor/Services/StartupConfigRewrite.cs create mode 100644 MCPForUnity/Editor/Services/StartupConfigRewrite.cs.meta create mode 100644 TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/StartupConfigRewriteTests.cs create mode 100644 TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/StartupConfigRewriteTests.cs.meta diff --git a/MCPForUnity/Editor/Services/StartupConfigRewrite.cs b/MCPForUnity/Editor/Services/StartupConfigRewrite.cs new file mode 100644 index 000000000..4de64f5b7 --- /dev/null +++ b/MCPForUnity/Editor/Services/StartupConfigRewrite.cs @@ -0,0 +1,54 @@ +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; + 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; + var before = c.Status; + if (before == McpStatus.NotConfigured) continue; + 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)."); + } + } +} 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/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/StartupConfigRewriteTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/StartupConfigRewriteTests.cs new file mode 100644 index 000000000..17eada4e5 --- /dev/null +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/StartupConfigRewriteTests.cs @@ -0,0 +1,37 @@ +using System.Reflection; +using NUnit.Framework; +using UnityEditor; + +namespace MCPForUnityTests.Editor.Services +{ + [TestFixture] + public class StartupConfigRewriteTests + { + [Test] + public void StartupConfigRewrite_TypeExists() + { + var t = System.Type.GetType("MCPForUnity.Editor.Services.StartupConfigRewrite, MCPForUnity.Editor"); + Assert.IsNotNull(t, "StartupConfigRewrite type must exist and be public"); + } + + [Test] + public void StartupConfigRewrite_HasInitializeOnLoad() + { + var t = System.Type.GetType("MCPForUnity.Editor.Services.StartupConfigRewrite, MCPForUnity.Editor"); + Assert.IsNotNull(t); + object[] attrs = t.GetCustomAttributes(typeof(InitializeOnLoadAttribute), inherit: false); + Assert.AreEqual(1, attrs.Length, "Class must be decorated with [InitializeOnLoad]"); + } + + [Test] + public void StartupConfigRewrite_RunOncePerSession_GuardKey() + { + var t = System.Type.GetType("MCPForUnity.Editor.Services.StartupConfigRewrite, MCPForUnity.Editor"); + var keyField = t?.GetField("SESSION_GUARD_KEY", + BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Public); + Assert.IsNotNull(keyField); + string val = (string)keyField.GetValue(null); + StringAssert.StartsWith("MCPForUnity.", val); + } + } +} diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/StartupConfigRewriteTests.cs.meta b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/StartupConfigRewriteTests.cs.meta new file mode 100644 index 000000000..76aac6555 --- /dev/null +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/StartupConfigRewriteTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ca5aeabe283e473ea5ae78cf60ee55f1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From 98c9df1431c85da1349de7051c65351e213649d5 Mon Sep 17 00:00:00 2001 From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com> Date: Mon, 18 May 2026 16:39:57 +0800 Subject: [PATCH 08/22] feat(setup): add client picker step to first-run wizard --- MCPForUnity/Editor/Windows/MCPSetupWindow.cs | 87 +++++++++++++++ .../Editor/Windows/MCPSetupWindow.uxml | 102 ++++++++++-------- 2 files changed, 146 insertions(+), 43 deletions(-) diff --git a/MCPForUnity/Editor/Windows/MCPSetupWindow.cs b/MCPForUnity/Editor/Windows/MCPSetupWindow.cs index 628a813d6..ef96e9e6e 100644 --- a/MCPForUnity/Editor/Windows/MCPSetupWindow.cs +++ b/MCPForUnity/Editor/Windows/MCPSetupWindow.cs @@ -1,7 +1,10 @@ using System; +using System.Collections.Generic; +using MCPForUnity.Editor.Clients; using MCPForUnity.Editor.Dependencies; using MCPForUnity.Editor.Dependencies.Models; using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Services; using UnityEditor; using UnityEngine; using UnityEngine.UIElements; @@ -28,6 +31,14 @@ public class MCPSetupWindow : EditorWindow private Button refreshButton; private Button doneButton; + // Step 2 (Configure Clients) UI elements + private VisualElement stepDeps; + private VisualElement stepClients; + private VisualElement clientsList; + private Button skipClientsButton; + private Button configureSelectedButton; + private readonly List<(IMcpClientConfigurator client, Toggle toggle)> clientToggles = new(); + private DependencyCheckResult _dependencyResult; public static void ShowWindow(DependencyCheckResult dependencyResult = null) @@ -69,12 +80,19 @@ public void CreateGUI() openUvLinkButton = rootVisualElement.Q