Skip to content

Commit c1df6c7

Browse files
authored
Merge pull request CoplayDev#1142 from Scriptwonder/fix/auto-test-multi
feat+fix: one-click client connection + autotest bug-fix batch
2 parents a384c7c + b70babf commit c1df6c7

35 files changed

Lines changed: 1005 additions & 255 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
.codeiumignore
66
.kiro
77

8+
# Local worktrees for parallel feature work
9+
.worktrees/
10+
811
# Code-copy related files
912
.clipignore
1013

MCPForUnity/Editor/Clients/Configurators/ClaudeDesktopConfigurator.cs

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.IO;
4-
using MCPForUnity.Editor.Constants;
54
using MCPForUnity.Editor.Models;
6-
using MCPForUnity.Editor.Services;
75
using UnityEditor;
86

97
namespace MCPForUnity.Editor.Clients.Configurators
@@ -39,27 +37,7 @@ public override string GetSkillInstallPath()
3937
"Save and restart Claude Desktop"
4038
};
4139

42-
public override void Configure()
43-
{
44-
bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport;
45-
if (useHttp)
46-
{
47-
throw new InvalidOperationException("Claude Desktop does not support HTTP transport. Switch to stdio in settings before configuring.");
48-
}
49-
50-
base.Configure();
51-
}
52-
53-
public override string GetManualSnippet()
54-
{
55-
bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport;
56-
if (useHttp)
57-
{
58-
return "# Claude Desktop does not support HTTP transport.\n" +
59-
"# In Connect tab, change the Transport option from HTTP to stdio, then regenerate.";
60-
}
61-
62-
return base.GetManualSnippet();
63-
}
40+
private static readonly ConfiguredTransport[] StdioOnly = { ConfiguredTransport.Stdio };
41+
public override IReadOnlyList<ConfiguredTransport> SupportedTransports => StdioOnly;
6442
}
6543
}

MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,19 @@ public interface IMcpClientConfigurator
2626
/// <summary>True if this client supports auto-configure.</summary>
2727
bool SupportsAutoConfigure { get; }
2828

29+
/// <summary>
30+
/// True if this client appears installed on the user's machine. Used to filter
31+
/// "configure all detected" so we don't write configs for apps the user doesn't have.
32+
/// Implementations should be cheap (filesystem stat or cached path lookup).
33+
/// </summary>
34+
bool IsInstalled { get; }
35+
36+
/// <summary>
37+
/// Transports this client can be configured with. Order is "preference if user has no opinion";
38+
/// the configure path picks the user's global preference if present in this list, else falls back to the first entry.
39+
/// </summary>
40+
System.Collections.Generic.IReadOnlyList<ConfiguredTransport> SupportedTransports { get; }
41+
2942
/// <summary>Label to show on the configure button for the current state.</summary>
3043
string GetConfigureActionLabel();
3144

MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ protected McpClientConfiguratorBase(McpClient client)
3030
public McpStatus Status => client.status;
3131
public ConfiguredTransport ConfiguredTransport => client.configuredTransport;
3232
public virtual bool SupportsAutoConfigure => true;
33+
// Default to a filesystem check on the configured path. Concrete configurators
34+
// whose presence isn't path-based (CLI binaries, etc.) override this. This makes
35+
// any future configurator that forgets to override fail-closed rather than be
36+
// treated as "detected" by ConfigureAllDetectedClients.
37+
public virtual bool IsInstalled => ParentDirectoryExists(GetConfigPath());
38+
private static readonly ConfiguredTransport[] DefaultTransports =
39+
{ ConfiguredTransport.Stdio, ConfiguredTransport.Http };
40+
public virtual IReadOnlyList<ConfiguredTransport> SupportedTransports => DefaultTransports;
3341
public virtual bool SupportsSkills => false;
3442
public virtual string GetConfigureActionLabel() => "Configure";
3543
public virtual string GetSkillInstallPath() => null;
@@ -59,6 +67,17 @@ protected string CurrentOsPath()
5967
return client.linuxConfigPath;
6068
}
6169

70+
protected static bool ParentDirectoryExists(string configPath)
71+
{
72+
try
73+
{
74+
if (string.IsNullOrEmpty(configPath)) return false;
75+
string parent = Path.GetDirectoryName(configPath);
76+
return !string.IsNullOrEmpty(parent) && Directory.Exists(parent);
77+
}
78+
catch { return false; }
79+
}
80+
6281
protected bool UrlsEqual(string a, string b)
6382
{
6483
if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b))
@@ -131,6 +150,8 @@ public JsonFileMcpConfigurator(McpClient client) : base(client) { }
131150

132151
public override string GetConfigPath() => CurrentOsPath();
133152

153+
public override bool IsInstalled => ParentDirectoryExists(GetConfigPath());
154+
134155
public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
135156
{
136157
try
@@ -148,43 +169,37 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
148169
string configuredUrl = null;
149170
bool configExists = false;
150171

151-
if (client.IsVsCodeLayout)
152172
{
153-
var vsConfig = JsonConvert.DeserializeObject<JToken>(configJson) as JObject;
154-
if (vsConfig != null)
173+
var rootConfig = JsonConvert.DeserializeObject<JToken>(configJson) as JObject;
174+
JToken unityToken = null;
175+
if (rootConfig != null)
155176
{
156-
var unityToken =
157-
vsConfig["servers"]?["unityMCP"]
158-
?? vsConfig["mcp"]?["servers"]?["unityMCP"];
177+
unityToken = client.IsVsCodeLayout
178+
? rootConfig["servers"]?["unityMCP"]
179+
?? rootConfig["mcp"]?["servers"]?["unityMCP"]
180+
: rootConfig["mcpServers"]?["unityMCP"];
181+
}
159182

160-
if (unityToken is JObject unityObj)
161-
{
162-
configExists = true;
183+
if (unityToken is JObject unityObj)
184+
{
185+
configExists = true;
163186

164-
var argsToken = unityObj["args"];
165-
if (argsToken is JArray)
166-
{
167-
args = argsToken.ToObject<string[]>();
168-
}
187+
var argsToken = unityObj["args"];
188+
if (argsToken is JArray)
189+
{
190+
args = argsToken.ToObject<string[]>();
191+
}
169192

170-
var urlToken = unityObj["url"] ?? unityObj["serverUrl"];
171-
if (urlToken != null && urlToken.Type != JTokenType.Null)
172-
{
173-
configuredUrl = urlToken.ToString();
174-
}
193+
// Clients diverge on the HTTP URL property name: "url" (Cursor/VSCode/Claude),
194+
// "serverUrl" (Antigravity/Windsurf), "httpUrl" (Gemini CLI). Accept all three
195+
// so CheckStatus matches what Configure() actually wrote.
196+
var urlToken = unityObj["url"] ?? unityObj["serverUrl"] ?? unityObj["httpUrl"];
197+
if (urlToken != null && urlToken.Type != JTokenType.Null)
198+
{
199+
configuredUrl = urlToken.ToString();
175200
}
176201
}
177202
}
178-
else
179-
{
180-
McpConfig standardConfig = JsonConvert.DeserializeObject<McpConfig>(configJson);
181-
if (standardConfig?.mcpServers?.unityMCP != null)
182-
{
183-
args = standardConfig.mcpServers.unityMCP.args;
184-
configuredUrl = standardConfig.mcpServers.unityMCP.url;
185-
configExists = true;
186-
}
187-
}
188203

189204
if (!configExists)
190205
{
@@ -357,6 +372,8 @@ public CodexMcpConfigurator(McpClient client) : base(client) { }
357372

358373
public override string GetConfigPath() => CurrentOsPath();
359374

375+
public override bool IsInstalled => ParentDirectoryExists(GetConfigPath());
376+
360377
public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
361378
{
362379
try
@@ -542,6 +559,8 @@ public ClaudeCliMcpConfigurator(McpClient client) : base(client) { }
542559

543560
public override string GetConfigPath() => "Managed via Claude CLI";
544561

562+
public override bool IsInstalled => MCPServiceLocator.Paths.IsClaudeCliDetected();
563+
545564
/// <summary>
546565
/// Returns the project directory that CLI-based configurators will use as the working directory
547566
/// for `claude mcp add/remove --scope local`. Checks for an explicit override in EditorPrefs

MCPForUnity/Editor/Constants/EditorPrefKeys.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ internal static class EditorPrefKeys
4949
internal const string ResourceFoldoutStatePrefix = "MCPForUnity.ResourceFoldout.";
5050
internal const string EditorWindowActivePanel = "MCPForUnity.EditorWindow.ActivePanel";
5151
internal const string LastSelectedClientId = "MCPForUnity.LastSelectedClientId";
52+
internal const string ClientDetailsFoldoutOpen = "MCPForUnity.ClientConfig.DetailsFoldoutOpen";
5253

5354
internal const string SetupCompleted = "MCPForUnity.SetupCompleted";
5455
internal const string SetupDismissed = "MCPForUnity.SetupDismissed";

MCPForUnity/Editor/MCPForUnity.Editor.asmdef

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414
],
1515
"autoReferenced": true,
1616
"defineConstraints": [],
17-
"versionDefines": [],
17+
"versionDefines": [
18+
{
19+
"name": "com.unity.visualeffectgraph",
20+
"expression": "0.0.0",
21+
"define": "UNITY_VFX_GRAPH"
22+
}
23+
],
1824
"noEngineReferences": false
1925
}

MCPForUnity/Editor/Services/ClientConfigurationService.cs

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public void ConfigureClient(IMcpClientConfigurator configurator)
3030
AssetPathUtility.CleanLocalServerBuildArtifacts();
3131
}
3232

33-
configurator.Configure();
33+
ConfigureWithTransportCoercion(configurator);
3434
}
3535

3636
public ClientConfigurationSummary ConfigureAllDetectedClients()
@@ -44,11 +44,16 @@ public ClientConfigurationSummary ConfigureAllDetectedClients()
4444
var summary = new ClientConfigurationSummary();
4545
foreach (var configurator in configurators)
4646
{
47+
if (!configurator.IsInstalled)
48+
{
49+
summary.SkippedCount++;
50+
continue;
51+
}
4752
try
4853
{
4954
// Always re-run configuration so core fields stay current
5055
configurator.CheckStatus(attemptAutoRewrite: false);
51-
configurator.Configure();
56+
ConfigureWithTransportCoercion(configurator);
5257
summary.SuccessCount++;
5358
summary.Messages.Add($"✓ {configurator.DisplayName}: Configured successfully");
5459
}
@@ -62,12 +67,80 @@ public ClientConfigurationSummary ConfigureAllDetectedClients()
6267
return summary;
6368
}
6469

70+
private static void ConfigureWithTransportCoercion(IMcpClientConfigurator configurator)
71+
{
72+
bool originalHttp = EditorConfigurationCache.Instance.UseHttpTransport;
73+
try
74+
{
75+
CoerceTransportFor(configurator);
76+
configurator.Configure();
77+
}
78+
finally
79+
{
80+
EditorConfigurationCache.Instance.SetUseHttpTransport(originalHttp);
81+
}
82+
}
83+
6584
public bool CheckClientStatus(IMcpClientConfigurator configurator, bool attemptAutoRewrite = true)
6685
{
6786
var previous = configurator.Status;
6887
var current = configurator.CheckStatus(attemptAutoRewrite);
6988
return current != previous;
7089
}
7190

91+
private static void CoerceTransportFor(IMcpClientConfigurator configurator)
92+
{
93+
var supported = configurator.SupportedTransports;
94+
if (supported == null || supported.Count == 0) return;
95+
96+
bool currentlyHttp = EditorConfigurationCache.Instance.UseHttpTransport;
97+
var requested = currentlyHttp ? ConfiguredTransport.Http : ConfiguredTransport.Stdio;
98+
99+
// Accept any HTTP variant (Http, HttpRemote) when the user wants HTTP — a client that
100+
// only supports HttpRemote should not get coerced to stdio just because Http isn't
101+
// explicitly listed.
102+
if (SupportsRequested(supported, requested)) return;
103+
104+
// Fall back in the direction of the user's intent: if they wanted HTTP, prefer any
105+
// HTTP variant the client does support; otherwise prefer stdio. Honors the
106+
// configurator's declared order when more than one option remains.
107+
ConfiguredTransport chosen = PickFallback(supported, requested);
108+
bool needHttp = IsHttpVariant(chosen);
109+
if (EditorConfigurationCache.Instance.UseHttpTransport != needHttp)
110+
{
111+
EditorConfigurationCache.Instance.SetUseHttpTransport(needHttp);
112+
McpLog.Info(
113+
$"[{configurator.DisplayName}] auto-selected {chosen} transport (client does not support {requested}).");
114+
}
115+
}
116+
117+
private static bool IsHttpVariant(ConfiguredTransport t)
118+
=> t == ConfiguredTransport.Http || t == ConfiguredTransport.HttpRemote;
119+
120+
private static bool SupportsRequested(IReadOnlyList<ConfiguredTransport> supported, ConfiguredTransport requested)
121+
{
122+
if (requested == ConfiguredTransport.Http)
123+
{
124+
foreach (var t in supported)
125+
if (IsHttpVariant(t)) return true;
126+
return false;
127+
}
128+
return supported.Contains(requested);
129+
}
130+
131+
private static ConfiguredTransport PickFallback(IReadOnlyList<ConfiguredTransport> supported, ConfiguredTransport requested)
132+
{
133+
if (requested == ConfiguredTransport.Http)
134+
{
135+
foreach (var t in supported)
136+
if (IsHttpVariant(t)) return t;
137+
}
138+
else
139+
{
140+
foreach (var t in supported)
141+
if (t == ConfiguredTransport.Stdio) return t;
142+
}
143+
return supported[0];
144+
}
72145
}
73146
}

0 commit comments

Comments
 (0)