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
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.
From c4e583fb6668d5c566b2623a8de1e273502d38ed Mon Sep 17 00:00:00 2001
From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com>
Date: Fri, 22 May 2026 13:28:25 +0800
Subject: [PATCH 14/22] fix(server): annotate execute_custom_tool parameters as
dict[str, Any] (#946)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
FastMCP derives the wire schema from each tool's type annotations.
execute_custom_tool declared `parameters: dict | None`, which generates
a permissive schema some strict clients (Roo Code, certain VSCode MCP
adapters) reject during tool discovery — the tool then appears missing
even though everything else is healthy.
Switch to `dict[str, Any] | None` so the generated schema includes
proper additionalProperties bounds. The four other files cited in #946
(custom_tool_service, manage_components, manage_material, manage_texture)
were already parametrised in prior commits; execute_custom_tool was the
last remaining MCP-exposed tool with a bare `dict` annotation.
---
Server/src/services/tools/execute_custom_tool.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/Server/src/services/tools/execute_custom_tool.py b/Server/src/services/tools/execute_custom_tool.py
index 60c1fff89..87b66409c 100644
--- a/Server/src/services/tools/execute_custom_tool.py
+++ b/Server/src/services/tools/execute_custom_tool.py
@@ -1,3 +1,5 @@
+from typing import Any
+
from fastmcp import Context
from mcp.types import ToolAnnotations
from models.models import MCPResponse
@@ -21,7 +23,7 @@
destructiveHint=True,
),
)
-async def execute_custom_tool(ctx: Context, tool_name: str, parameters: dict | None = None) -> MCPResponse:
+async def execute_custom_tool(ctx: Context, tool_name: str, parameters: dict[str, Any] | None = None) -> MCPResponse:
unity_instance = await get_unity_instance_from_context(ctx)
if not unity_instance:
return MCPResponse(
From 8946d8efc9857e132c133f9af1367ad8371a353d Mon Sep 17 00:00:00 2001
From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com>
Date: Fri, 22 May 2026 13:31:05 +0800
Subject: [PATCH 15/22] chore(stdio): gate per-connection lifecycle logs behind
verbose (#865)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Multi-agent and sub-agent workflows churn through stdio connections —
each new agent triggers a connect → close-stale → exit cycle, so the
three associated McpLog.Info calls in StdioBridgeHost flooded the Unity
console (the reporter saw 185+ lines in a single session). They're
useful for debugging a single hung connection but pure noise during
normal multi-agent operation.
Move all three to always:false so they only surface when verbose
logging is on, matching the existing recv-frame log on the same path.
Lifecycle messages that fire once (startup, port switch) and warnings
about anomalies (TCS timeout, queue eviction) are left at INFO.
---
.../Editor/Services/Transport/Transports/StdioBridgeHost.cs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs b/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs
index 0e9674d65..1eb4f0771 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);
}
}
}
From 29711c17560adf8c79bcb043bfe9eced180bdcc5 Mon Sep 17 00:00:00 2001
From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com>
Date: Fri, 22 May 2026 15:01:00 +0800
Subject: [PATCH 16/22] ui(client-config): lead with Configure All, collapse
per-client details
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Now that "Configure All Detected Clients" actually does what its name
says (auto-rewrite + per-client transport coercion + IsInstalled
filtering all landed in the recent client-config work), it's the path
we want first-run users on — not buried at the bottom of the panel.
Layout changes in McpClientConfigSection.uxml:
- Move the Configure-All button to the top of the section, right under
the "Client Configuration" header, with a one-line helper underneath.
- Wrap the dropdown / status / single-client Configure button / Claude
CLI path / project-dir / Manual Configuration foldout in a new
"Configure a single client" foldout, collapsed by default. Persist
its open/closed state via a new EditorPrefs key.
Style changes in Common.uss:
- New .primary-button class (bright green, 34px, bold) for the
one-click action so it visually distinguishes itself from the regular
blue .action-button rows.
- Light/dark-aware foldout header styling for the new client-details
foldout that matches the existing manual-command-foldout treatment.
---
.../Editor/Constants/EditorPrefKeys.cs | 1 +
.../ClientConfig/McpClientConfigSection.cs | 13 ++++
.../ClientConfig/McpClientConfigSection.uxml | 77 ++++++++++---------
.../Editor/Windows/Components/Common.uss | 62 +++++++++++++++
4 files changed, 116 insertions(+), 37 deletions(-)
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/Windows/Components/ClientConfig/McpClientConfigSection.cs b/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs
index b3f7b7386..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("browse-project-dir-button");
clearProjectDirButton = Root.Q("clear-project-dir-button");
+ clientDetailsFoldout = Root.Q("client-details-foldout");
manualConfigFoldout = Root.Q("manual-config-foldout");
configPathField = Root.Q("config-path");
copyPathButton = Root.Q("copy-path-button");
@@ -109,6 +111,17 @@ private void InitializeUI()
manualConfigFoldout.value = false;
}
+ // Restore the "Configure a single client" foldout state. Defaults to collapsed
+ // so the prominent "Configure All Detected Clients" path is what users see first.
+ if (clientDetailsFoldout != null)
+ {
+ clientDetailsFoldout.value = EditorPrefs.GetBool(EditorPrefKeys.ClientDetailsFoldoutOpen, false);
+ clientDetailsFoldout.RegisterValueChangedCallback(evt =>
+ {
+ EditorPrefs.SetBool(EditorPrefKeys.ClientDetailsFoldoutOpen, evt.newValue);
+ });
+ }
+
var clientNames = configurators.Select(c => c.DisplayName).ToList();
clientDropdown.choices = clientNames;
if (clientNames.Count > 0)
diff --git a/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.uxml b/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.uxml
index e1a9deb02..2498fa613 100644
--- a/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.uxml
+++ b/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.uxml
@@ -3,47 +3,50 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
diff --git a/MCPForUnity/Editor/Windows/Components/Common.uss b/MCPForUnity/Editor/Windows/Components/Common.uss
index 24a69d01c..a6cc6d616 100644
--- a/MCPForUnity/Editor/Windows/Components/Common.uss
+++ b/MCPForUnity/Editor/Windows/Components/Common.uss
@@ -241,6 +241,68 @@
background-color: rgba(80, 80, 80, 0.5);
}
+/* Primary one-click action — flagged as the recommended path. */
+.primary-button {
+ width: 100%;
+ height: 34px;
+ margin-top: 4px;
+ margin-bottom: 4px;
+ background-color: rgba(0, 175, 110, 1);
+ color: rgba(255, 255, 255, 1);
+ border-radius: 6px;
+ border-width: 1px;
+ border-color: rgba(0, 145, 90, 1);
+ font-size: 13px;
+ -unity-font-style: bold;
+ letter-spacing: 0.3px;
+}
+
+.primary-button:hover {
+ background-color: rgba(0, 200, 130, 1);
+ border-color: rgba(0, 170, 105, 1);
+}
+
+.primary-button:active {
+ background-color: rgba(0, 150, 90, 1);
+ border-color: rgba(0, 125, 75, 1);
+}
+
+.primary-button:disabled {
+ background-color: rgba(70, 100, 85, 0.5);
+ color: rgba(220, 220, 220, 0.6);
+ border-color: rgba(70, 100, 85, 0.5);
+}
+
+.primary-button-hint {
+ margin-top: 0;
+ margin-bottom: 8px;
+ -unity-text-align: middle-left;
+ color: rgba(150, 165, 160, 1);
+}
+
+/* Foldout that hides the per-client (single-target) controls. */
+.client-details-foldout {
+ margin-top: 4px;
+ margin-bottom: 4px;
+}
+
+.client-details-foldout > .unity-foldout__toggle {
+ -unity-font-style: bold;
+ font-size: 11px;
+ padding: 4px;
+ background-color: rgba(0, 0, 0, 0.05);
+ border-radius: 3px;
+}
+
+.client-details-foldout > .unity-foldout__content {
+ margin-top: 6px;
+ padding-left: 4px;
+}
+
+.unity-theme-light .client-details-foldout > .unity-foldout__toggle {
+ background-color: rgba(0, 0, 0, 0.03);
+}
+
/* Manual Configuration */
.manual-config-content {
padding: 8px;
From 9c01c4a3db3853ee3d431e401dc81405c6de0625 Mon Sep 17 00:00:00 2001
From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com>
Date: Fri, 22 May 2026 15:05:22 +0800
Subject: [PATCH 17/22] ui(client-config): drop the helper text, rename foldout
to "Per-client setup"
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Helper line under the green button was visual noise — the button label
already says what it does. "Configure a single client" was redundant
inside a section titled "Client Configuration"; "Per-client setup"
reads cleaner. Also drop the now-orphan .primary-button-hint style.
---
.../Components/ClientConfig/McpClientConfigSection.uxml | 3 +--
MCPForUnity/Editor/Windows/Components/Common.uss | 7 -------
2 files changed, 1 insertion(+), 9 deletions(-)
diff --git a/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.uxml b/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.uxml
index 2498fa613..fa21af6db 100644
--- a/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.uxml
+++ b/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.uxml
@@ -4,8 +4,7 @@
-
-
+
diff --git a/MCPForUnity/Editor/Windows/Components/Common.uss b/MCPForUnity/Editor/Windows/Components/Common.uss
index a6cc6d616..ecffd6a9c 100644
--- a/MCPForUnity/Editor/Windows/Components/Common.uss
+++ b/MCPForUnity/Editor/Windows/Components/Common.uss
@@ -273,13 +273,6 @@
border-color: rgba(70, 100, 85, 0.5);
}
-.primary-button-hint {
- margin-top: 0;
- margin-bottom: 8px;
- -unity-text-align: middle-left;
- color: rgba(150, 165, 160, 1);
-}
-
/* Foldout that hides the per-client (single-target) controls. */
.client-details-foldout {
margin-top: 4px;
From e2b570d938aa382ab6bbe0b57262c7dae1dd109b Mon Sep 17 00:00:00 2001
From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com>
Date: Fri, 22 May 2026 15:31:54 +0800
Subject: [PATCH 18/22] fix(vfx): wire UNITY_VFX_GRAPH via versionDefines and
drop 12.1-only allowlist
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The whole manage_vfx tool dispatcher and every VfxGraph* helper is
gated behind '#if UNITY_VFX_GRAPH', but nothing in the Editor asmdef
ever defined that symbol. Users with com.unity.visualeffectgraph
properly installed (e.g. Unity 6.3 + VFX Graph 17.x) hit the false
"VFX Graph package (com.unity.visualeffectgraph) not installed" branch
for every action, including read-only ones like list_templates and
get_info — reported via Discord.
Add a versionDefines entry to MCPForUnity.Editor.asmdef so Unity sets
UNITY_VFX_GRAPH whenever the VFX Graph package is present at any
version (`expression: 0.0.0`). This is the canonical way to detect
optional packages and lets the existing #if branches do their job.
Also drop ValidateVfxGraphVersion's hard-coded {"12.1"} allowlist,
which only matches Unity 2022.3-era VFX Graph and would still block
CreateAsset on modern installs even after the compile-gate fix. The
asset-level APIs we touch (VisualEffectAsset, AssetDatabase.CopyAsset,
template enumeration via PackageInfo) are stable across the
12.x → 17.x range, so the safer guard is just "package present" with
the compile-time gate handling the real "not installed" path.
---
MCPForUnity/Editor/MCPForUnity.Editor.asmdef | 8 ++-
.../Editor/Tools/Vfx/VfxGraphAssets.cs | 49 ++-----------------
2 files changed, 12 insertions(+), 45 deletions(-)
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/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)
From 643670d9131c0a1c418079ae7def9dec2fc8c6d2 Mon Sep 17 00:00:00 2001
From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com>
Date: Fri, 22 May 2026 15:46:37 +0800
Subject: [PATCH 19/22] fix(setup): unstick per-dependency Install/Uninstall
buttons on UPM failure
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
AddDependencyRow's click handlers flipped the button to "Installing..."
and only restored it inside a synchronous try/catch. But the per-package
call sites passed `() => InstallUpmPackage(...)` — no onComplete — so
when the UPM AddAndRemove request eventually completed (success OR
failure), nothing in PollUpmRequest's continuation ever told the button
to revert. A network timeout on com.unity.cinemachine therefore parked
the button on "Installing..." forever (Discord report); same for
Removing... on uninstall. The "Install All" path was fine because it
already threaded an onComplete callback through.
Change the install/uninstall hooks to Action, thread a `restore`
callback from the click handler into the UPM helpers, and invoke it
from PollUpmRequest's completion regardless of StatusCode. Roslyn (the
one synchronous install) just invokes the callback inline after Install
returns. The unrelated CRLF/LF noise in the diff is a side effect of
normalizing the file (988 CRLF lines, 41 LF lines pre-existing) onto
the dominant CRLF convention while my edits were in flight.
---
.../Editor/Windows/MCPForUnityEditorWindow.cs | 122 ++++++++++--------
1 file changed, 66 insertions(+), 56 deletions(-)
diff --git a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
index 9dcfb7343..c934fa0d7 100644
--- a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
+++ b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
@@ -71,45 +71,45 @@ internal static void CloseAllWindows()
}
}
- public static void ShowWindow()
- {
- var existingWindows = UnityEngine.Resources.FindObjectsOfTypeAll();
- MCPForUnityEditorWindow window = null;
-
- if (existingWindows.Length > 0)
- {
- window = existingWindows[0];
-
- // If multiple instances exist, keep one and close the extras to avoid stale hidden tabs.
- for (int i = 1; i < existingWindows.Length; i++)
- {
- try
- {
- existingWindows[i].Close();
- }
- catch (Exception ex)
- {
- McpLog.Warn($"Error closing duplicate MCP window: {ex.Message}");
- }
- }
- }
- else
- {
- window = GetWindow("MCP For Unity");
- }
-
- window.titleContent = new GUIContent("MCP For Unity");
- window.minSize = new Vector2(500, 340);
-
- if (window.position.width < 100 || window.position.height < 100)
- {
- window.position = new Rect(120, 120, 900, 700);
- }
-
- window.Show();
- window.ShowTab();
- window.Focus();
- }
+ public static void ShowWindow()
+ {
+ var existingWindows = UnityEngine.Resources.FindObjectsOfTypeAll();
+ MCPForUnityEditorWindow window = null;
+
+ if (existingWindows.Length > 0)
+ {
+ window = existingWindows[0];
+
+ // If multiple instances exist, keep one and close the extras to avoid stale hidden tabs.
+ for (int i = 1; i < existingWindows.Length; i++)
+ {
+ try
+ {
+ existingWindows[i].Close();
+ }
+ catch (Exception ex)
+ {
+ McpLog.Warn($"Error closing duplicate MCP window: {ex.Message}");
+ }
+ }
+ }
+ else
+ {
+ window = GetWindow("MCP For Unity");
+ }
+
+ window.titleContent = new GUIContent("MCP For Unity");
+ window.minSize = new Vector2(500, 340);
+
+ if (window.position.width < 100 || window.position.height < 100)
+ {
+ window.position = new Rect(120, 120, 900, 700);
+ }
+
+ window.Show();
+ window.ShowTab();
+ window.Focus();
+ }
// Helper to check and manage open windows from other classes
public static bool HasAnyOpenWindow()
@@ -159,8 +159,8 @@ public void CreateGUI()
return;
}
- rootVisualElement.Clear();
- visualTree.CloneTree(rootVisualElement);
+ rootVisualElement.Clear();
+ visualTree.CloneTree(rootVisualElement);
// Load main window USS
var mainStyleSheet = AssetDatabase.LoadAssetAtPath(
@@ -832,8 +832,10 @@ private static void BuildDependenciesSection(VisualElement container)
? "Installed via Plugins/Roslyn \u2014 execute_code uses Roslyn"
: "Available (loaded from NuGet/external) \u2014 execute_code uses Roslyn",
"Not installed \u2014 execute_code falls back to C# 6 (CodeDom)",
- () => RoslynInstaller.Install(interactive: true),
- roslynInstalledLocally ? (Action)(() => UninstallRoslyn()) : null);
+ done => { RoslynInstaller.Install(interactive: true); done?.Invoke(); },
+ roslynInstalledLocally
+ ? (Action)(done => { UninstallRoslyn(); done?.Invoke(); })
+ : null);
// ProBuilder
bool hasProBuilder = Type.GetType("UnityEngine.ProBuilder.ProBuilderMesh, Unity.ProBuilder") != null;
@@ -843,8 +845,8 @@ private static void BuildDependenciesSection(VisualElement container)
hasProBuilder,
"Installed",
"Not installed",
- () => InstallUpmPackage("com.unity.probuilder"),
- () => RemoveUpmPackage("com.unity.probuilder"));
+ done => InstallUpmPackage("com.unity.probuilder", done),
+ done => RemoveUpmPackage("com.unity.probuilder", done));
// Cinemachine
bool hasCinemachine = Type.GetType("Unity.Cinemachine.CinemachineCamera, Unity.Cinemachine") != null
@@ -855,8 +857,8 @@ private static void BuildDependenciesSection(VisualElement container)
hasCinemachine,
"Installed",
"Not installed \u2014 camera tool works without it",
- () => InstallUpmPackage("com.unity.cinemachine"),
- () => RemoveUpmPackage("com.unity.cinemachine"));
+ done => InstallUpmPackage("com.unity.cinemachine", done),
+ done => RemoveUpmPackage("com.unity.cinemachine", done));
// VFX Graph — uses preprocessor symbol, so check via UPM package list
bool hasVfxGraph = IsUpmPackageInstalled("com.unity.visualeffectgraph");
@@ -866,8 +868,8 @@ private static void BuildDependenciesSection(VisualElement container)
hasVfxGraph,
"Installed",
"Not installed \u2014 VFX tool falls back to ParticleSystem/LineRenderer",
- () => InstallUpmPackage("com.unity.visualeffectgraph"),
- () => RemoveUpmPackage("com.unity.visualeffectgraph"));
+ done => InstallUpmPackage("com.unity.visualeffectgraph", done),
+ done => RemoveUpmPackage("com.unity.visualeffectgraph", done));
section.Add(content);
container.Add(section);
@@ -875,7 +877,7 @@ private static void BuildDependenciesSection(VisualElement container)
private static void AddDependencyRow(VisualElement parent, string name, string description,
bool isInstalled, string installedText, string missingText,
- Action installAction, Action uninstallAction)
+ Action installAction, Action uninstallAction)
{
var row = new VisualElement();
row.style.marginBottom = 8;
@@ -921,12 +923,16 @@ private static void AddDependencyRow(VisualElement parent, string name, string d
{
btn.SetEnabled(false);
btn.text = "Installing...";
- try { installAction(); }
- catch (Exception e)
+ Action restore = () =>
{
- Debug.LogError($"[MCP] Install failed: {e.Message}");
btn.SetEnabled(true);
btn.text = "Install";
+ };
+ try { installAction(restore); }
+ catch (Exception e)
+ {
+ Debug.LogError($"[MCP] Install failed: {e.Message}");
+ restore();
}
});
btn.text = "Install";
@@ -943,12 +949,16 @@ private static void AddDependencyRow(VisualElement parent, string name, string d
$"Are you sure you want to remove {name}?", "Remove", "Cancel")) return;
btn.SetEnabled(false);
btn.text = "Removing...";
- try { uninstallAction(); }
- catch (Exception e)
+ Action restore = () =>
{
- Debug.LogError($"[MCP] Uninstall failed: {e.Message}");
btn.SetEnabled(true);
btn.text = "Uninstall";
+ };
+ try { uninstallAction(restore); }
+ catch (Exception e)
+ {
+ Debug.LogError($"[MCP] Uninstall failed: {e.Message}");
+ restore();
}
});
btn.text = "Uninstall";
From d29f2132abb815892d5ecf84e1a20adcf5b13ff3 Mon Sep 17 00:00:00 2001
From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com>
Date: Fri, 22 May 2026 19:36:23 +0800
Subject: [PATCH 20/22] fix(clients): transport-coercion HttpRemote +
StartupConfigRewrite safety (PR review)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Two correctness bugs flagged on PR #1142 (Copilot + CodeRabbit).
CoerceTransportFor only treated `ConfiguredTransport.Http` as an HTTP
choice, so a client that declares `SupportedTransports = { HttpRemote }`
would get flipped to stdio whenever the user preferred HTTP. The
fallback selection was also order-dependent (`supported[0]`), which
could pick stdio over Http when the user wanted HTTP and the client
supported both. Now: any HTTP variant satisfies an HTTP request, and
when we must fall back we prefer the variant that matches user intent
(HTTP family vs stdio).
StartupConfigRewrite shared two problems with the CommandRegistry path
from #1134: it fired in the AssetImportWorker subprocess too (where
half-loaded domains can crash Mono on reflection and where writing
client configs from a worker is plain wrong), and it skipped rewrite
when in-memory Status was NotConfigured *before* refreshing from disk
— on a fresh editor load that miscategorized clients that were already
configured on disk and prevented the auto-rewrite. Added the same
AssetImportWorker guard as #1134 (reflective lookup + cmdline fallback)
and dropped the pre-refresh status skip so CheckStatus always reads
the file before deciding.
---
.../Services/ClientConfigurationService.cs | 41 +++++++++++++++--
.../Editor/Services/StartupConfigRewrite.cs | 44 ++++++++++++++++++-
2 files changed, 81 insertions(+), 4 deletions(-)
diff --git a/MCPForUnity/Editor/Services/ClientConfigurationService.cs b/MCPForUnity/Editor/Services/ClientConfigurationService.cs
index 45de0fbe6..d3eb86693 100644
--- a/MCPForUnity/Editor/Services/ClientConfigurationService.cs
+++ b/MCPForUnity/Editor/Services/ClientConfigurationService.cs
@@ -96,10 +96,16 @@ private static void CoerceTransportFor(IMcpClientConfigurator configurator)
bool currentlyHttp = EditorConfigurationCache.Instance.UseHttpTransport;
var requested = currentlyHttp ? ConfiguredTransport.Http : ConfiguredTransport.Stdio;
- if (supported.Contains(requested)) return; // user preference is supported, no change
+ // 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;
- var chosen = supported[0];
- bool needHttp = chosen == ConfiguredTransport.Http;
+ // 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);
@@ -107,5 +113,34 @@ private static void CoerceTransportFor(IMcpClientConfigurator configurator)
$"[{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
index 4de64f5b7..2df69f3a3 100644
--- a/MCPForUnity/Editor/Services/StartupConfigRewrite.cs
+++ b/MCPForUnity/Editor/Services/StartupConfigRewrite.cs
@@ -1,3 +1,5 @@
+using System;
+using System.Reflection;
using MCPForUnity.Editor.Clients;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Helpers;
@@ -20,6 +22,10 @@ public static class StartupConfigRewrite
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;
}
@@ -37,8 +43,10 @@ private static void RunOnce()
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;
- if (before == McpStatus.NotConfigured) continue;
var after = c.CheckStatus(attemptAutoRewrite: true);
if (before != after && after == McpStatus.Configured) rewrote++;
}
@@ -50,5 +58,39 @@ private static void RunOnce()
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;
+ }
}
}
From 71083d2e1b3c0d3cf299ff2abed06c9c82214081 Mon Sep 17 00:00:00 2001
From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com>
Date: Fri, 22 May 2026 19:37:32 +0800
Subject: [PATCH 21/22] fix: tighten IsInstalled default + accept
parameters=None on execute_custom_tool (PR review)
Two review nits from PR #1142.
McpClientConfiguratorBase.IsInstalled defaulted to `true`, which meant
any future configurator that derives directly from the base (without
going through JsonFile/Codex/ClaudeCli) would be treated as "detected"
by ConfigureAllDetectedClients and could end up writing config files
for apps that aren't on the machine. Default to a cheap filesystem
check via ParentDirectoryExists(GetConfigPath()); the three existing
base classes that override with the same check are harmlessly redundant
now, and CLI configurators (where GetConfigPath isn't a real path) keep
their own overrides.
execute_custom_tool declared `parameters: dict[str, Any] | None = None`
in its signature but then rejected `None` at runtime with
"parameters must be an object/dictionary". For parameter-less custom
tools the type hint and the behavior contradicted each other. Coerce
`None` to an empty dict; reject only genuinely-wrong types.
---
MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs | 6 +++++-
Server/src/services/tools/execute_custom_tool.py | 6 +++++-
2 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
index 57dab8f67..5c3d81c7c 100644
--- a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
+++ b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
@@ -30,7 +30,11 @@ protected McpClientConfiguratorBase(McpClient client)
public McpStatus Status => client.status;
public ConfiguredTransport ConfiguredTransport => client.configuredTransport;
public virtual bool SupportsAutoConfigure => true;
- public virtual bool IsInstalled => 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;
diff --git a/Server/src/services/tools/execute_custom_tool.py b/Server/src/services/tools/execute_custom_tool.py
index 87b66409c..38544b132 100644
--- a/Server/src/services/tools/execute_custom_tool.py
+++ b/Server/src/services/tools/execute_custom_tool.py
@@ -38,7 +38,11 @@ async def execute_custom_tool(ctx: Context, tool_name: str, parameters: dict[str
message=f"Could not resolve project id for {unity_instance}. Ensure Unity is running and reachable.",
)
- if not isinstance(parameters, dict):
+ # The signature accepts None (parameter-less custom tools). Treat it as an empty
+ # dict rather than rejecting — the previous behavior contradicted the optional type.
+ if parameters is None:
+ parameters = {}
+ elif not isinstance(parameters, dict):
return MCPResponse(
success=False,
message="parameters must be an object/dictionary",
From b70babfa98b4ae0f923aafb09a9e6b869f79cf02 Mon Sep 17 00:00:00 2001
From: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com>
Date: Fri, 22 May 2026 19:39:22 +0800
Subject: [PATCH 22/22] fix: wizard empty-selection, README transport
contradiction, test polish (PR review)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Four smaller items flagged on PR #1142.
MCPSetupWindow.OnConfigureSelectedClicked marked setup as completed
and closed the window even when the user hadn't ticked any client —
clicking "Configure Selected" with everything unchecked would silently
skip setup forever. Show a prompt and return early instead.
README listed Claude Desktop under the HTTP-default group while a
neighboring paragraph correctly says Claude Desktop is stdio-only;
removed it from the HTTP list and pointed at the stdio block.
StartupConfigRewrite_TypeExists only checked that the type resolved,
not that it's public — added an explicit IsPublic assertion so the
test matches its own message about the [InitializeOnLoad] requirement.
ConfigureDetectedClientsTests calls the real ClientConfigurationService
which walks McpClientRegistry and Configure()s every detected client —
on CI this is harmless (no clients installed) but on a dev machine it
mutates real user config files. Marked both tests [Explicit] until we
DI the configurator list through the service.
---
MCPForUnity/Editor/Windows/MCPSetupWindow.cs | 8 ++++++++
README.md | 2 +-
.../EditMode/Services/ConfigureDetectedClientsTests.cs | 7 +++++++
.../Tests/EditMode/Services/StartupConfigRewriteTests.cs | 3 ++-
4 files changed, 18 insertions(+), 2 deletions(-)
diff --git a/MCPForUnity/Editor/Windows/MCPSetupWindow.cs b/MCPForUnity/Editor/Windows/MCPSetupWindow.cs
index ef96e9e6e..502085640 100644
--- a/MCPForUnity/Editor/Windows/MCPSetupWindow.cs
+++ b/MCPForUnity/Editor/Windows/MCPSetupWindow.cs
@@ -179,6 +179,14 @@ private void OnConfigureSelectedClicked()
messages.Add($"⚠ {c.DisplayName}: {ex.Message}");
}
}
+ if (success == 0 && failure == 0)
+ {
+ EditorUtility.DisplayDialog(
+ "Client Configuration",
+ "No clients were selected. Tick at least one client to continue, or close the window to skip setup.",
+ "OK");
+ return;
+ }
EditorUtility.DisplayDialog(
"Client Configuration",
$"{success} configured, {failure} failed.\n\n{string.Join("\n", messages)}",
diff --git a/README.md b/README.md
index 1d1d5dacc..2a27a18fa 100644
--- a/README.md
+++ b/README.md
@@ -118,7 +118,7 @@ openupm add com.coplaydev.unity-mcp
If auto-setup doesn't work, add this to your MCP client's config file:
-**HTTP (default — works with Claude Desktop, Cursor, Windsurf):**
+**HTTP (default — works with Cursor, Windsurf, Antigravity, VS Code, Cline; Claude Desktop is stdio-only, see below):**
```json
{
"mcpServers": {
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ConfigureDetectedClientsTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ConfigureDetectedClientsTests.cs
index a0ef9d111..576ccfa53 100644
--- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ConfigureDetectedClientsTests.cs
+++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/ConfigureDetectedClientsTests.cs
@@ -4,10 +4,16 @@
namespace MCPForUnityTests.Editor.Services
{
+ // ConfigureAllDetectedClients walks the real McpClientRegistry and Configure()s every
+ // detected client, which would touch real user config files on a dev machine. These
+ // tests pass on CI (no MCP clients installed there) but would mutate real state on
+ // a developer's machine. Marked [Explicit] so they only run when invoked by name;
+ // proper isolation requires DI of the configurator list and is tracked separately.
[TestFixture]
public class ConfigureDetectedClientsTests
{
[Test]
+ [Explicit("Side-effect: writes real client configs on machines with MCP clients installed")]
public void Summary_ContainsOnlyInstalledClients()
{
var svc = new ClientConfigurationService();
@@ -18,6 +24,7 @@ public void Summary_ContainsOnlyInstalledClients()
}
[Test]
+ [Explicit("Side-effect: writes real client configs on machines with MCP clients installed")]
public void Summary_SkippedCountTracksUninstalled()
{
var svc = new ClientConfigurationService();
diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/StartupConfigRewriteTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/StartupConfigRewriteTests.cs
index 17eada4e5..eff93be53 100644
--- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/StartupConfigRewriteTests.cs
+++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Services/StartupConfigRewriteTests.cs
@@ -11,7 +11,8 @@ public class StartupConfigRewriteTests
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");
+ Assert.IsNotNull(t, "StartupConfigRewrite type must exist");
+ Assert.IsTrue(t.IsPublic, "StartupConfigRewrite must be public so the [InitializeOnLoad] attribute fires");
}
[Test]