Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d933770
chore: ignore .worktrees/ for local parallel feature work
Scriptwonder May 18, 2026
69be612
feat(clients): add IsInstalled detection to configurator contract
Scriptwonder May 18, 2026
fe43e61
refactor(clients): DRY IsInstalled overrides using shared helpers
Scriptwonder May 18, 2026
6b064a1
feat(clients): per-client SupportedTransports with auto-coercion
Scriptwonder May 18, 2026
17dbf42
fix(clients): apply transport coercion in bulk configure path
Scriptwonder May 18, 2026
03daf97
feat(clients): filter ConfigureAll to only detected clients
Scriptwonder May 18, 2026
1bde9aa
feat(clients): startup auto-rewrite of stale client configs
Scriptwonder May 18, 2026
98c9df1
feat(setup): add client picker step to first-run wizard
Scriptwonder May 18, 2026
71a9cd4
docs: describe first-run client auto-configuration
Scriptwonder May 18, 2026
6b59fc9
docs: correct button label and Claude Desktop transport notes
Scriptwonder May 18, 2026
534a1bf
fix(clients): recognize serverUrl/httpUrl in CheckStatus
Scriptwonder May 21, 2026
c903626
fix(serialization): hide UnityEngineObjectConverter from global disco…
Scriptwonder May 21, 2026
3ad6dc5
fix(registry): skip AutoDiscover in AssetImportWorker and guard refle…
Scriptwonder May 21, 2026
c4e583f
fix(server): annotate execute_custom_tool parameters as dict[str, Any…
Scriptwonder May 22, 2026
8946d8e
chore(stdio): gate per-connection lifecycle logs behind verbose (#865)
Scriptwonder May 22, 2026
29711c1
ui(client-config): lead with Configure All, collapse per-client details
Scriptwonder May 22, 2026
9c01c4a
ui(client-config): drop the helper text, rename foldout to "Per-clien…
Scriptwonder May 22, 2026
e2b570d
fix(vfx): wire UNITY_VFX_GRAPH via versionDefines and drop 12.1-only …
Scriptwonder May 22, 2026
643670d
fix(setup): unstick per-dependency Install/Uninstall buttons on UPM f…
Scriptwonder May 22, 2026
324c484
Merge remote-tracking branch 'upstream/beta' into fix/auto-test-multi
Scriptwonder May 22, 2026
abfc66a
Merge remote-tracking branch 'upstream/beta' into fix/auto-test-multi
Scriptwonder May 22, 2026
d29f213
fix(clients): transport-coercion HttpRemote + StartupConfigRewrite sa…
Scriptwonder May 22, 2026
71083d2
fix: tighten IsInstalled default + accept parameters=None on execute_…
Scriptwonder May 22, 2026
b70babf
fix: wizard empty-selection, README transport contradiction, test pol…
Scriptwonder May 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
.codeiumignore
.kiro

# Local worktrees for parallel feature work
.worktrees/

# Code-copy related files
.clipignore

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -39,27 +37,7 @@ public override string GetSkillInstallPath()
"Save and restart Claude Desktop"
};

public override void Configure()
{
bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport;
if (useHttp)
{
throw new InvalidOperationException("Claude Desktop does not support HTTP transport. Switch to stdio in settings before configuring.");
}

base.Configure();
}

public override string GetManualSnippet()
{
bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport;
if (useHttp)
{
return "# Claude Desktop does not support HTTP transport.\n" +
"# In Connect tab, change the Transport option from HTTP to stdio, then regenerate.";
}

return base.GetManualSnippet();
}
private static readonly ConfiguredTransport[] StdioOnly = { ConfiguredTransport.Stdio };
public override IReadOnlyList<ConfiguredTransport> SupportedTransports => StdioOnly;
}
}
13 changes: 13 additions & 0 deletions MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ public interface IMcpClientConfigurator
/// <summary>True if this client supports auto-configure.</summary>
bool SupportsAutoConfigure { get; }

/// <summary>
/// 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).
/// </summary>
bool IsInstalled { get; }

/// <summary>
/// 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.
/// </summary>
System.Collections.Generic.IReadOnlyList<ConfiguredTransport> SupportedTransports { get; }

/// <summary>Label to show on the configure button for the current state.</summary>
string GetConfigureActionLabel();

Expand Down
77 changes: 48 additions & 29 deletions MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ protected McpClientConfiguratorBase(McpClient client)
public McpStatus Status => client.status;
public ConfiguredTransport ConfiguredTransport => client.configuredTransport;
public virtual bool SupportsAutoConfigure => true;
// Default to a filesystem check on the configured path. Concrete configurators
// whose presence isn't path-based (CLI binaries, etc.) override this. This makes
// any future configurator that forgets to override fail-closed rather than be
// treated as "detected" by ConfigureAllDetectedClients.
public virtual bool IsInstalled => ParentDirectoryExists(GetConfigPath());
private static readonly ConfiguredTransport[] DefaultTransports =
{ ConfiguredTransport.Stdio, ConfiguredTransport.Http };
public virtual IReadOnlyList<ConfiguredTransport> SupportedTransports => DefaultTransports;
public virtual bool SupportsSkills => false;
Comment on lines 30 to 41
public virtual string GetConfigureActionLabel() => "Configure";
public virtual string GetSkillInstallPath() => null;
Expand Down Expand Up @@ -59,6 +67,17 @@ protected string CurrentOsPath()
return client.linuxConfigPath;
}

protected static bool ParentDirectoryExists(string configPath)
{
try
{
if (string.IsNullOrEmpty(configPath)) return false;
string parent = Path.GetDirectoryName(configPath);
return !string.IsNullOrEmpty(parent) && Directory.Exists(parent);
}
catch { return false; }
}

protected bool UrlsEqual(string a, string b)
{
if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b))
Expand Down Expand Up @@ -131,6 +150,8 @@ public JsonFileMcpConfigurator(McpClient client) : base(client) { }

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

public override bool IsInstalled => ParentDirectoryExists(GetConfigPath());

public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
{
try
Expand All @@ -148,43 +169,37 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
string configuredUrl = null;
bool configExists = false;

if (client.IsVsCodeLayout)
{
var vsConfig = JsonConvert.DeserializeObject<JToken>(configJson) as JObject;
if (vsConfig != null)
var rootConfig = JsonConvert.DeserializeObject<JToken>(configJson) as JObject;
JToken unityToken = null;
if (rootConfig != null)
{
var unityToken =
vsConfig["servers"]?["unityMCP"]
?? vsConfig["mcp"]?["servers"]?["unityMCP"];
unityToken = client.IsVsCodeLayout
? rootConfig["servers"]?["unityMCP"]
?? rootConfig["mcp"]?["servers"]?["unityMCP"]
: rootConfig["mcpServers"]?["unityMCP"];
}

if (unityToken is JObject unityObj)
{
configExists = true;
if (unityToken is JObject unityObj)
{
configExists = true;

var argsToken = unityObj["args"];
if (argsToken is JArray)
{
args = argsToken.ToObject<string[]>();
}
var argsToken = unityObj["args"];
if (argsToken is JArray)
{
args = argsToken.ToObject<string[]>();
}

var urlToken = unityObj["url"] ?? unityObj["serverUrl"];
if (urlToken != null && urlToken.Type != JTokenType.Null)
{
configuredUrl = urlToken.ToString();
}
// Clients diverge on the HTTP URL property name: "url" (Cursor/VSCode/Claude),
// "serverUrl" (Antigravity/Windsurf), "httpUrl" (Gemini CLI). Accept all three
// so CheckStatus matches what Configure() actually wrote.
var urlToken = unityObj["url"] ?? unityObj["serverUrl"] ?? unityObj["httpUrl"];
if (urlToken != null && urlToken.Type != JTokenType.Null)
{
configuredUrl = urlToken.ToString();
}
}
}
else
{
McpConfig standardConfig = JsonConvert.DeserializeObject<McpConfig>(configJson);
if (standardConfig?.mcpServers?.unityMCP != null)
{
args = standardConfig.mcpServers.unityMCP.args;
configuredUrl = standardConfig.mcpServers.unityMCP.url;
configExists = true;
}
}

if (!configExists)
{
Expand Down Expand Up @@ -357,6 +372,8 @@ public CodexMcpConfigurator(McpClient client) : base(client) { }

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

public override bool IsInstalled => ParentDirectoryExists(GetConfigPath());

public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
{
try
Expand Down Expand Up @@ -542,6 +559,8 @@ public ClaudeCliMcpConfigurator(McpClient client) : base(client) { }

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

public override bool IsInstalled => MCPServiceLocator.Paths.IsClaudeCliDetected();

/// <summary>
/// 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
Expand Down
1 change: 1 addition & 0 deletions MCPForUnity/Editor/Constants/EditorPrefKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
8 changes: 7 additions & 1 deletion MCPForUnity/Editor/MCPForUnity.Editor.asmdef
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"versionDefines": [
{
"name": "com.unity.visualeffectgraph",
"expression": "0.0.0",
"define": "UNITY_VFX_GRAPH"
}
],
"noEngineReferences": false
}
77 changes: 75 additions & 2 deletions MCPForUnity/Editor/Services/ClientConfigurationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public void ConfigureClient(IMcpClientConfigurator configurator)
AssetPathUtility.CleanLocalServerBuildArtifacts();
}

configurator.Configure();
ConfigureWithTransportCoercion(configurator);
}

public ClientConfigurationSummary ConfigureAllDetectedClients()
Expand All @@ -44,11 +44,16 @@ public ClientConfigurationSummary ConfigureAllDetectedClients()
var summary = new ClientConfigurationSummary();
foreach (var configurator in configurators)
{
if (!configurator.IsInstalled)
{
summary.SkippedCount++;
continue;
}
try
{
// Always re-run configuration so core fields stay current
configurator.CheckStatus(attemptAutoRewrite: false);
configurator.Configure();
ConfigureWithTransportCoercion(configurator);
summary.SuccessCount++;
summary.Messages.Add($"✓ {configurator.DisplayName}: Configured successfully");
}
Expand All @@ -62,12 +67,80 @@ 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;
var current = configurator.CheckStatus(attemptAutoRewrite);
return current != previous;
}

private static void CoerceTransportFor(IMcpClientConfigurator configurator)
{
var supported = configurator.SupportedTransports;
if (supported == null || supported.Count == 0) return;

bool currentlyHttp = EditorConfigurationCache.Instance.UseHttpTransport;
var requested = currentlyHttp ? ConfiguredTransport.Http : ConfiguredTransport.Stdio;

// Accept any HTTP variant (Http, HttpRemote) when the user wants HTTP — a client that
// only supports HttpRemote should not get coerced to stdio just because Http isn't
// explicitly listed.
if (SupportsRequested(supported, requested)) return;

// Fall back in the direction of the user's intent: if they wanted HTTP, prefer any
// HTTP variant the client does support; otherwise prefer stdio. Honors the
// configurator's declared order when more than one option remains.
ConfiguredTransport chosen = PickFallback(supported, requested);
bool needHttp = IsHttpVariant(chosen);
if (EditorConfigurationCache.Instance.UseHttpTransport != needHttp)
{
EditorConfigurationCache.Instance.SetUseHttpTransport(needHttp);
McpLog.Info(
$"[{configurator.DisplayName}] auto-selected {chosen} transport (client does not support {requested}).");
}
Comment on lines +96 to +114
}

private static bool IsHttpVariant(ConfiguredTransport t)
=> t == ConfiguredTransport.Http || t == ConfiguredTransport.HttpRemote;

private static bool SupportsRequested(IReadOnlyList<ConfiguredTransport> 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<ConfiguredTransport> 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];
}
}
}
Loading
Loading