From 7426d2415907f56223a25cd2195927eff9996b31 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Thu, 27 Nov 2025 01:14:58 -0400 Subject: [PATCH 01/14] First pass at MCP client refactor --- MCPForUnity/Editor/Clients.meta | 8 + .../Editor/Clients/ClaudeCodeConfigurator.cs | 24 + .../ClaudeCodeConfigurator.cs.meta} | 2 +- .../Clients/ClaudeDesktopConfigurator.cs | 53 ++ .../ClaudeDesktopConfigurator.cs.meta} | 2 +- .../Editor/Clients/CodexConfigurator.cs | 26 + .../Editor/Clients/CodexConfigurator.cs.meta | 11 + .../Editor/Clients/CursorConfigurator.cs | 27 + .../Editor/Clients/CursorConfigurator.cs.meta | 11 + .../Editor/Clients/IMcpClientConfigurator.cs | 41 ++ .../Clients/IMcpClientConfigurator.cs.meta | 11 + .../Editor/Clients/KiroConfigurator.cs | 29 + .../Editor/Clients/KiroConfigurator.cs.meta | 11 + .../Clients/McpClientConfiguratorBase.cs | 548 +++++++++++++++++ .../Clients/McpClientConfiguratorBase.cs.meta | 11 + .../Editor/Clients/McpClientRegistry.cs | 59 ++ .../Editor/Clients/McpClientRegistry.cs.meta | 11 + .../Editor/Clients/TraeConfigurator.cs | 26 + .../Editor/Clients/TraeConfigurator.cs.meta | 11 + .../Editor/Clients/VSCodeConfigurator.cs | 28 + .../Editor/Clients/VSCodeConfigurator.cs.meta | 11 + .../Editor/Clients/WindsurfConfigurator.cs | 36 ++ .../Clients/WindsurfConfigurator.cs.meta | 11 + MCPForUnity/Editor/Data/McpClients.cs | 225 ------- .../Editor/Helpers/ConfigJsonBuilder.cs | 48 +- .../Editor/Helpers/McpConfigurationHelper.cs | 2 +- .../Migrations/StdIoVersionMigration.cs | 45 +- MCPForUnity/Editor/Models/McpClient.cs | 11 +- MCPForUnity/Editor/Models/McpTypes.cs | 14 - .../Services/ClientConfigurationService.cs | 563 +----------------- .../Services/IClientConfigurationService.cs | 38 +- .../Services/ServerManagementService.cs | 1 - .../ClientConfig/McpClientConfigSection.cs | 99 +-- .../Editor/Windows/MCPForUnityEditorWindow.cs | 13 +- .../EditMode/Helpers/WriteToConfigTests.cs | 75 ++- 35 files changed, 1185 insertions(+), 957 deletions(-) create mode 100644 MCPForUnity/Editor/Clients.meta create mode 100644 MCPForUnity/Editor/Clients/ClaudeCodeConfigurator.cs rename MCPForUnity/Editor/{Data/McpClients.cs.meta => Clients/ClaudeCodeConfigurator.cs.meta} (83%) create mode 100644 MCPForUnity/Editor/Clients/ClaudeDesktopConfigurator.cs rename MCPForUnity/Editor/{Models/McpTypes.cs.meta => Clients/ClaudeDesktopConfigurator.cs.meta} (83%) create mode 100644 MCPForUnity/Editor/Clients/CodexConfigurator.cs create mode 100644 MCPForUnity/Editor/Clients/CodexConfigurator.cs.meta create mode 100644 MCPForUnity/Editor/Clients/CursorConfigurator.cs create mode 100644 MCPForUnity/Editor/Clients/CursorConfigurator.cs.meta create mode 100644 MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs create mode 100644 MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs.meta create mode 100644 MCPForUnity/Editor/Clients/KiroConfigurator.cs create mode 100644 MCPForUnity/Editor/Clients/KiroConfigurator.cs.meta create mode 100644 MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs create mode 100644 MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs.meta create mode 100644 MCPForUnity/Editor/Clients/McpClientRegistry.cs create mode 100644 MCPForUnity/Editor/Clients/McpClientRegistry.cs.meta create mode 100644 MCPForUnity/Editor/Clients/TraeConfigurator.cs create mode 100644 MCPForUnity/Editor/Clients/TraeConfigurator.cs.meta create mode 100644 MCPForUnity/Editor/Clients/VSCodeConfigurator.cs create mode 100644 MCPForUnity/Editor/Clients/VSCodeConfigurator.cs.meta create mode 100644 MCPForUnity/Editor/Clients/WindsurfConfigurator.cs create mode 100644 MCPForUnity/Editor/Clients/WindsurfConfigurator.cs.meta delete mode 100644 MCPForUnity/Editor/Data/McpClients.cs delete mode 100644 MCPForUnity/Editor/Models/McpTypes.cs diff --git a/MCPForUnity/Editor/Clients.meta b/MCPForUnity/Editor/Clients.meta new file mode 100644 index 000000000..b4105b364 --- /dev/null +++ b/MCPForUnity/Editor/Clients.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c9d47f01d06964ee7843765d1bd71205 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Clients/ClaudeCodeConfigurator.cs b/MCPForUnity/Editor/Clients/ClaudeCodeConfigurator.cs new file mode 100644 index 000000000..215c2dca3 --- /dev/null +++ b/MCPForUnity/Editor/Clients/ClaudeCodeConfigurator.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using MCPForUnity.Editor.Models; + +namespace MCPForUnity.Editor.Clients +{ + public class ClaudeCodeConfigurator : ClaudeCliMcpConfigurator + { + public ClaudeCodeConfigurator() : base(new McpClient + { + name = "Claude Code", + windowsConfigPath = string.Empty, + macConfigPath = string.Empty, + linuxConfigPath = string.Empty, + }) + { } + + public override IList GetInstallationSteps() => new List + { + "Ensure Claude CLI is installed", + "Use Register to add UnityMCP (or run claude mcp add UnityMCP)", + "Restart Claude Code" + }; + } +} diff --git a/MCPForUnity/Editor/Data/McpClients.cs.meta b/MCPForUnity/Editor/Clients/ClaudeCodeConfigurator.cs.meta similarity index 83% rename from MCPForUnity/Editor/Data/McpClients.cs.meta rename to MCPForUnity/Editor/Clients/ClaudeCodeConfigurator.cs.meta index e5a10813f..b5ceb3fc7 100644 --- a/MCPForUnity/Editor/Data/McpClients.cs.meta +++ b/MCPForUnity/Editor/Clients/ClaudeCodeConfigurator.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 711b86bbc1f661e4fb2c822e14970e16 +guid: d0d22681fc594475db1c189f2d9abdf7 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/MCPForUnity/Editor/Clients/ClaudeDesktopConfigurator.cs b/MCPForUnity/Editor/Clients/ClaudeDesktopConfigurator.cs new file mode 100644 index 000000000..e1678244d --- /dev/null +++ b/MCPForUnity/Editor/Clients/ClaudeDesktopConfigurator.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.IO; +using MCPForUnity.Editor.Constants; +using MCPForUnity.Editor.Models; +using UnityEditor; + +namespace MCPForUnity.Editor.Clients +{ + public class ClaudeDesktopConfigurator : JsonFileMcpConfigurator + { + public ClaudeDesktopConfigurator() : base(new McpClient + { + name = "Claude Desktop", + windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Claude", "claude_desktop_config.json"), + macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Claude", "claude_desktop_config.json"), + linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Claude", "claude_desktop_config.json"), + SupportsHttpTransport = false + }) + { } + + public override IList GetInstallationSteps() => new List + { + "Open Claude Desktop", + "Settings > Developer > Edit Config (or open path)", + "Paste JSON", + "Save and restart" + }; + + public override void Configure() + { + bool useHttp = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true); + 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 = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true); + if (useHttp) + { + return "# Claude Desktop does not support HTTP transport.\n" + + "# Open Advanced Settings and disable HTTP transport to use stdio, then regenerate."; + } + + return base.GetManualSnippet(); + } + } +} diff --git a/MCPForUnity/Editor/Models/McpTypes.cs.meta b/MCPForUnity/Editor/Clients/ClaudeDesktopConfigurator.cs.meta similarity index 83% rename from MCPForUnity/Editor/Models/McpTypes.cs.meta rename to MCPForUnity/Editor/Clients/ClaudeDesktopConfigurator.cs.meta index 377a6d0be..905c262d0 100644 --- a/MCPForUnity/Editor/Models/McpTypes.cs.meta +++ b/MCPForUnity/Editor/Clients/ClaudeDesktopConfigurator.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 9ca97c5ff5ed74c4fbb65cfa9d2bfed1 +guid: d5e5d87c9db57495f842dc366f1ebd65 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/MCPForUnity/Editor/Clients/CodexConfigurator.cs b/MCPForUnity/Editor/Clients/CodexConfigurator.cs new file mode 100644 index 000000000..8cd512d49 --- /dev/null +++ b/MCPForUnity/Editor/Clients/CodexConfigurator.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.IO; +using MCPForUnity.Editor.Models; + +namespace MCPForUnity.Editor.Clients +{ + public class CodexConfigurator : CodexMcpConfigurator + { + public CodexConfigurator() : base(new McpClient + { + name = "Codex", + windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codex", "config.toml"), + macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codex", "config.toml"), + linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codex", "config.toml") + }) + { } + + public override IList GetInstallationSteps() => new List + { + "Run 'codex config edit' or open the config path", + "Paste the TOML", + "Save and restart Codex" + }; + } +} diff --git a/MCPForUnity/Editor/Clients/CodexConfigurator.cs.meta b/MCPForUnity/Editor/Clients/CodexConfigurator.cs.meta new file mode 100644 index 000000000..14bc60e47 --- /dev/null +++ b/MCPForUnity/Editor/Clients/CodexConfigurator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c7037ef8b168e49f79247cb31c3be75a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Clients/CursorConfigurator.cs b/MCPForUnity/Editor/Clients/CursorConfigurator.cs new file mode 100644 index 000000000..42d67acec --- /dev/null +++ b/MCPForUnity/Editor/Clients/CursorConfigurator.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.IO; +using MCPForUnity.Editor.Models; + +namespace MCPForUnity.Editor.Clients +{ + public class CursorConfigurator : JsonFileMcpConfigurator + { + public CursorConfigurator() : base(new McpClient + { + name = "Cursor", + windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cursor", "mcp.json"), + macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cursor", "mcp.json"), + linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cursor", "mcp.json") + }) + { } + + public override IList GetInstallationSteps() => new List + { + "Open Cursor", + "Settings > MCP > Add new global MCP server", + "Paste config JSON", + "Save and restart" + }; + } +} diff --git a/MCPForUnity/Editor/Clients/CursorConfigurator.cs.meta b/MCPForUnity/Editor/Clients/CursorConfigurator.cs.meta new file mode 100644 index 000000000..578eb0f64 --- /dev/null +++ b/MCPForUnity/Editor/Clients/CursorConfigurator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b708eda314746481fb8f4a1fb0652b03 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs b/MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs new file mode 100644 index 000000000..02bc41e72 --- /dev/null +++ b/MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs @@ -0,0 +1,41 @@ +using MCPForUnity.Editor.Models; + +namespace MCPForUnity.Editor.Clients +{ + /// + /// Contract for MCP client configurators. Each client is responsible for + /// status detection, auto-configure, and manual snippet/steps. + /// + public interface IMcpClientConfigurator + { + /// Stable identifier (e.g., "cursor"). + string Id { get; } + + /// Display name shown in the UI. + string DisplayName { get; } + + /// Current status cached by the configurator. + McpStatus Status { get; } + + /// True if this client supports auto-configure. + bool SupportsAutoConfigure { get; } + + /// Label to show on the configure button for the current state. + string GetConfigureActionLabel(); + + /// Returns the platform-specific config path (or message for CLI-managed clients). + string GetConfigPath(); + + /// Checks and updates status; returns current status. + McpStatus CheckStatus(bool attemptAutoRewrite = true); + + /// Runs auto-configuration (register/write file/CLI etc.). + void Configure(); + + /// Returns the manual configuration snippet (JSON/TOML/commands). + string GetManualSnippet(); + + /// Returns ordered human-readable installation steps. + System.Collections.Generic.IList GetInstallationSteps(); + } +} diff --git a/MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs.meta b/MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs.meta new file mode 100644 index 000000000..fc5739664 --- /dev/null +++ b/MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f5a5078d9e6e14027a1abfebf4018634 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Clients/KiroConfigurator.cs b/MCPForUnity/Editor/Clients/KiroConfigurator.cs new file mode 100644 index 000000000..c14d07b28 --- /dev/null +++ b/MCPForUnity/Editor/Clients/KiroConfigurator.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.IO; +using MCPForUnity.Editor.Models; + +namespace MCPForUnity.Editor.Clients +{ + public class KiroConfigurator : JsonFileMcpConfigurator + { + public KiroConfigurator() : base(new McpClient + { + name = "Kiro", + windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kiro", "settings", "mcp.json"), + macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kiro", "settings", "mcp.json"), + linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kiro", "settings", "mcp.json"), + EnsureEnvObject = true, + DefaultUnityFields = { { "disabled", false } } + }) + { } + + public override IList GetInstallationSteps() => new List + { + "Open Kiro", + "Settings > search \"MCP\" > Open Workspace MCP Config", + "Paste JSON", + "Save and restart" + }; + } +} diff --git a/MCPForUnity/Editor/Clients/KiroConfigurator.cs.meta b/MCPForUnity/Editor/Clients/KiroConfigurator.cs.meta new file mode 100644 index 000000000..dacb04359 --- /dev/null +++ b/MCPForUnity/Editor/Clients/KiroConfigurator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e9b73ff071a6043dda1f2ec7d682ef71 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs new file mode 100644 index 000000000..503b2ef91 --- /dev/null +++ b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs @@ -0,0 +1,548 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using MCPForUnity.Editor.Constants; +using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Models; +using MCPForUnity.Editor.Services; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEngine; + +namespace MCPForUnity.Editor.Clients +{ + /// Shared base class for MCP configurators. + public abstract class McpClientConfiguratorBase : IMcpClientConfigurator + { + protected readonly McpClient client; + + protected McpClientConfiguratorBase(McpClient client) + { + this.client = client; + } + + internal McpClient Client => client; + + public string Id => client.name.Replace(" ", "").ToLowerInvariant(); + public virtual string DisplayName => client.name; + public McpStatus Status => client.status; + public virtual bool SupportsAutoConfigure => true; + public virtual string GetConfigureActionLabel() => "Configure"; + + public abstract string GetConfigPath(); + public abstract McpStatus CheckStatus(bool attemptAutoRewrite = true); + public abstract void Configure(); + public abstract string GetManualSnippet(); + public abstract IList GetInstallationSteps(); + + protected string GetUvxPathOrError() + { + string uvx = MCPServiceLocator.Paths.GetUvxPath(); + if (string.IsNullOrEmpty(uvx)) + { + throw new InvalidOperationException("uv not found. Install uv/uvx or set the override in Advanced Settings."); + } + return uvx; + } + + protected string CurrentOsPath() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return client.windowsConfigPath; + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + return client.macConfigPath; + return client.linuxConfigPath; + } + + protected bool UrlsEqual(string a, string b) + { + if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b)) + { + return false; + } + + if (Uri.TryCreate(a.Trim(), UriKind.Absolute, out var uriA) && + Uri.TryCreate(b.Trim(), UriKind.Absolute, out var uriB)) + { + return Uri.Compare( + uriA, + uriB, + UriComponents.HttpRequestUrl, + UriFormat.SafeUnescaped, + StringComparison.OrdinalIgnoreCase) == 0; + } + + string Normalize(string value) => value.Trim().TrimEnd('/'); + return string.Equals(Normalize(a), Normalize(b), StringComparison.OrdinalIgnoreCase); + } + } + + /// JSON-file based configurator (Cursor, Windsurf, VS Code, etc.). + public abstract class JsonFileMcpConfigurator : McpClientConfiguratorBase + { + public JsonFileMcpConfigurator(McpClient client) : base(client) { } + + public override string GetConfigPath() => CurrentOsPath(); + + public override McpStatus CheckStatus(bool attemptAutoRewrite = true) + { + try + { + string path = GetConfigPath(); + if (!File.Exists(path)) + { + client.SetStatus(McpStatus.NotConfigured); + return client.status; + } + + string configJson = File.ReadAllText(path); + string[] args = null; + string configuredUrl = null; + bool configExists = false; + + if (client.IsVsCodeLayout) + { + var vsConfig = JsonConvert.DeserializeObject(configJson) as JObject; + if (vsConfig != null) + { + var unityToken = + vsConfig["servers"]?["unityMCP"] + ?? vsConfig["mcp"]?["servers"]?["unityMCP"]; + + if (unityToken is JObject unityObj) + { + configExists = true; + + var argsToken = unityObj["args"]; + if (argsToken is JArray) + { + args = argsToken.ToObject(); + } + + var urlToken = unityObj["url"] ?? unityObj["serverUrl"]; + if (urlToken != null && urlToken.Type != JTokenType.Null) + { + configuredUrl = urlToken.ToString(); + } + } + } + } + else + { + McpConfig standardConfig = JsonConvert.DeserializeObject(configJson); + if (standardConfig?.mcpServers?.unityMCP != null) + { + args = standardConfig.mcpServers.unityMCP.args; + configExists = true; + } + } + + if (!configExists) + { + client.SetStatus(McpStatus.MissingConfig); + return client.status; + } + + bool matches = false; + if (args != null && args.Length > 0) + { + string expectedUvxUrl = AssetPathUtility.GetMcpServerGitUrl(); + string configuredUvxUrl = McpConfigurationHelper.ExtractUvxUrl(args); + matches = !string.IsNullOrEmpty(configuredUvxUrl) && + McpConfigurationHelper.PathsEqual(configuredUvxUrl, expectedUvxUrl); + } + else if (!string.IsNullOrEmpty(configuredUrl)) + { + string expectedUrl = HttpEndpointUtility.GetMcpRpcUrl(); + matches = UrlsEqual(configuredUrl, expectedUrl); + } + + if (matches) + { + client.SetStatus(McpStatus.Configured); + return client.status; + } + + if (attemptAutoRewrite) + { + var result = McpConfigurationHelper.WriteMcpConfiguration(path, client); + if (result == "Configured successfully") + { + client.SetStatus(McpStatus.Configured); + } + else + { + client.SetStatus(McpStatus.IncorrectPath); + } + } + else + { + client.SetStatus(McpStatus.IncorrectPath); + } + } + catch (Exception ex) + { + client.SetStatus(McpStatus.Error, ex.Message); + } + + return client.status; + } + + public override void Configure() + { + string path = GetConfigPath(); + McpConfigurationHelper.EnsureConfigDirectoryExists(path); + string result = McpConfigurationHelper.WriteMcpConfiguration(path, client); + if (result == "Configured successfully") + { + client.SetStatus(McpStatus.Configured); + } + else + { + throw new InvalidOperationException(result); + } + } + + public override string GetManualSnippet() + { + try + { + string uvx = GetUvxPathOrError(); + return ConfigJsonBuilder.BuildManualConfigJson(uvx, client); + } + catch (Exception ex) + { + return $"{{ \"error\": \"{ex.Message}\" }}"; + } + } + + public override IList GetInstallationSteps() => new List { "Configuration steps not available for this client." }; + } + + /// Codex (TOML) configurator. + public abstract class CodexMcpConfigurator : McpClientConfiguratorBase + { + public CodexMcpConfigurator(McpClient client) : base(client) { } + + public override string GetConfigPath() => CurrentOsPath(); + + public override McpStatus CheckStatus(bool attemptAutoRewrite = true) + { + try + { + string path = GetConfigPath(); + if (!File.Exists(path)) + { + client.SetStatus(McpStatus.NotConfigured); + return client.status; + } + + string toml = File.ReadAllText(path); + if (CodexConfigHelper.TryParseCodexServer(toml, out _, out var args, out var url)) + { + bool matches = false; + if (!string.IsNullOrEmpty(url)) + { + matches = UrlsEqual(url, HttpEndpointUtility.GetMcpRpcUrl()); + } + else if (args != null && args.Length > 0) + { + string expected = AssetPathUtility.GetMcpServerGitUrl(); + string configured = McpConfigurationHelper.ExtractUvxUrl(args); + matches = !string.IsNullOrEmpty(configured) && + McpConfigurationHelper.PathsEqual(configured, expected); + } + + if (matches) + { + client.SetStatus(McpStatus.Configured); + return client.status; + } + } + + if (attemptAutoRewrite) + { + string result = McpConfigurationHelper.ConfigureCodexClient(path, client); + if (result == "Configured successfully") + { + client.SetStatus(McpStatus.Configured); + } + else + { + client.SetStatus(McpStatus.IncorrectPath); + } + } + else + { + client.SetStatus(McpStatus.IncorrectPath); + } + } + catch (Exception ex) + { + client.SetStatus(McpStatus.Error, ex.Message); + } + + return client.status; + } + + public override void Configure() + { + string path = GetConfigPath(); + McpConfigurationHelper.EnsureConfigDirectoryExists(path); + string result = McpConfigurationHelper.ConfigureCodexClient(path, client); + if (result == "Configured successfully") + { + client.SetStatus(McpStatus.Configured); + } + else + { + throw new InvalidOperationException(result); + } + } + + public override string GetManualSnippet() + { + try + { + string uvx = GetUvxPathOrError(); + return CodexConfigHelper.BuildCodexServerBlock(uvx); + } + catch (Exception ex) + { + return $"# error: {ex.Message}"; + } + } + + public override IList GetInstallationSteps() => new List + { + "Run 'codex config edit' or open the config path", + "Paste the TOML", + "Save and restart Codex" + }; + } + + /// CLI-based configurator (Claude Code). + public abstract class ClaudeCliMcpConfigurator : McpClientConfiguratorBase + { + public ClaudeCliMcpConfigurator(McpClient client) : base(client) { } + + public override bool SupportsAutoConfigure => true; + public override string GetConfigureActionLabel() => client.status == McpStatus.Configured ? "Unregister" : "Register"; + + public override string GetConfigPath() => "Managed via Claude CLI"; + + public override McpStatus CheckStatus(bool attemptAutoRewrite = true) + { + try + { + var pathService = MCPServiceLocator.Paths; + string claudePath = pathService.GetClaudeCliPath(); + + if (string.IsNullOrEmpty(claudePath)) + { + client.SetStatus(McpStatus.NotConfigured, "Claude CLI not found"); + return client.status; + } + + string args = "mcp list"; + string projectDir = Path.GetDirectoryName(Application.dataPath); + + string pathPrepend = null; + if (Application.platform == RuntimePlatform.OSXEditor) + { + pathPrepend = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"; + } + else if (Application.platform == RuntimePlatform.LinuxEditor) + { + pathPrepend = "/usr/local/bin:/usr/bin:/bin"; + } + + try + { + string claudeDir = Path.GetDirectoryName(claudePath); + if (!string.IsNullOrEmpty(claudeDir)) + { + pathPrepend = string.IsNullOrEmpty(pathPrepend) + ? claudeDir + : $"{claudeDir}:{pathPrepend}"; + } + } + catch { } + + if (ExecPath.TryRun(claudePath, args, projectDir, out var stdout, out _, 10000, pathPrepend)) + { + if (!string.IsNullOrEmpty(stdout) && stdout.IndexOf("UnityMCP", StringComparison.OrdinalIgnoreCase) >= 0) + { + client.SetStatus(McpStatus.Configured); + return client.status; + } + } + + client.SetStatus(McpStatus.NotConfigured); + } + catch (Exception ex) + { + client.SetStatus(McpStatus.Error, ex.Message); + } + + return client.status; + } + + public override void Configure() + { + if (client.status == McpStatus.Configured) + { + Unregister(); + } + else + { + Register(); + } + } + + private void Register() + { + var pathService = MCPServiceLocator.Paths; + string claudePath = pathService.GetClaudeCliPath(); + if (string.IsNullOrEmpty(claudePath)) + { + throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first."); + } + + bool useHttpTransport = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true); + + string args; + if (useHttpTransport) + { + string httpUrl = HttpEndpointUtility.GetMcpRpcUrl(); + args = $"mcp add --transport http UnityMCP {httpUrl}"; + } + else + { + var (uvxPath, gitUrl, packageName) = AssetPathUtility.GetUvxCommandParts(); + args = $"mcp add --transport stdio UnityMCP -- \"{uvxPath}\" --from \"{gitUrl}\" {packageName}"; + } + + string projectDir = Path.GetDirectoryName(Application.dataPath); + + string pathPrepend = null; + if (Application.platform == RuntimePlatform.OSXEditor) + { + pathPrepend = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"; + } + else if (Application.platform == RuntimePlatform.LinuxEditor) + { + pathPrepend = "/usr/local/bin:/usr/bin:/bin"; + } + + try + { + string claudeDir = Path.GetDirectoryName(claudePath); + if (!string.IsNullOrEmpty(claudeDir)) + { + pathPrepend = string.IsNullOrEmpty(pathPrepend) + ? claudeDir + : $"{claudeDir}:{pathPrepend}"; + } + } + catch { } + + bool already = false; + if (!ExecPath.TryRun(claudePath, args, projectDir, out var stdout, out var stderr, 15000, pathPrepend)) + { + string combined = ($"{stdout}\n{stderr}") ?? string.Empty; + if (combined.IndexOf("already exists", StringComparison.OrdinalIgnoreCase) >= 0) + { + already = true; + } + else + { + throw new InvalidOperationException($"Failed to register with Claude Code:\n{stderr}\n{stdout}"); + } + } + + if (!already) + { + McpLog.Info("Successfully registered with Claude Code."); + } + + CheckStatus(); + } + + private void Unregister() + { + var pathService = MCPServiceLocator.Paths; + string claudePath = pathService.GetClaudeCliPath(); + + if (string.IsNullOrEmpty(claudePath)) + { + throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first."); + } + + string projectDir = Path.GetDirectoryName(Application.dataPath); + string pathPrepend = Application.platform == RuntimePlatform.OSXEditor + ? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin" + : null; + + bool serverExists = ExecPath.TryRun(claudePath, "mcp get UnityMCP", projectDir, out _, out _, 7000, pathPrepend); + + if (!serverExists) + { + client.SetStatus(McpStatus.NotConfigured); + McpLog.Info("No MCP for Unity server found - already unregistered."); + return; + } + + if (ExecPath.TryRun(claudePath, "mcp remove UnityMCP", projectDir, out var stdout, out var stderr, 10000, pathPrepend)) + { + McpLog.Info("MCP server successfully unregistered from Claude Code."); + } + else + { + throw new InvalidOperationException($"Failed to unregister: {stderr}"); + } + + client.SetStatus(McpStatus.NotConfigured); + CheckStatus(); + } + + public override string GetManualSnippet() + { + string uvxPath = MCPServiceLocator.Paths.GetUvxPath(); + bool useHttpTransport = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true); + + if (useHttpTransport) + { + string httpUrl = HttpEndpointUtility.GetMcpRpcUrl(); + return "# Register the MCP server with Claude Code:\n" + + $"claude mcp add --transport http UnityMCP {httpUrl}\n\n" + + "# Unregister the MCP server:\n" + + "claude mcp remove UnityMCP\n\n" + + "# List registered servers:\n" + + "claude mcp list # Only works when claude is run in the project's directory"; + } + + if (string.IsNullOrEmpty(uvxPath)) + { + return "# Error: Configuration not available - check paths in Advanced Settings"; + } + + string gitUrl = AssetPathUtility.GetMcpServerGitUrl(); + return "# Register the MCP server with Claude Code:\n" + + $"claude mcp add --transport stdio UnityMCP -- \"{uvxPath}\" --from \"{gitUrl}\" mcp-for-unity\n\n" + + "# Unregister the MCP server:\n" + + "claude mcp remove UnityMCP\n\n" + + "# List registered servers:\n" + + "claude mcp list # Only works when claude is run in the project's directory"; + } + + public override IList GetInstallationSteps() => new List + { + "Ensure Claude CLI is installed", + "Use Register to add UnityMCP (or run claude mcp add UnityMCP)", + "Restart Claude Code" + }; + } +} diff --git a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs.meta b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs.meta new file mode 100644 index 000000000..17709d19a --- /dev/null +++ b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8d408fd7733cb4a1eb80f785307db2ff +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Clients/McpClientRegistry.cs b/MCPForUnity/Editor/Clients/McpClientRegistry.cs new file mode 100644 index 000000000..b29c2f809 --- /dev/null +++ b/MCPForUnity/Editor/Clients/McpClientRegistry.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; + +namespace MCPForUnity.Editor.Clients +{ + /// + /// Central registry that auto-discovers configurators via TypeCache. + /// + public static class McpClientRegistry + { + private static List cached; + + public static IReadOnlyList All + { + get + { + if (cached == null) + { + cached = BuildRegistry(); + } + return cached; + } + } + + private static List BuildRegistry() + { + var configurators = new List(); + + foreach (var type in TypeCache.GetTypesDerivedFrom()) + { + if (type.IsAbstract || !type.IsClass || !type.IsPublic) + continue; + + // Require a public parameterless constructor + if (type.GetConstructor(Type.EmptyTypes) == null) + continue; + + try + { + if (Activator.CreateInstance(type) is IMcpClientConfigurator instance) + { + configurators.Add(instance); + } + } + catch (Exception ex) + { + Debug.LogWarning($"UnityMCP: Failed to instantiate configurator {type.Name}: {ex.Message}"); + } + } + + // Alphabetical order by display name + configurators = configurators.OrderBy(c => c.DisplayName, StringComparer.OrdinalIgnoreCase).ToList(); + return configurators; + } + } +} diff --git a/MCPForUnity/Editor/Clients/McpClientRegistry.cs.meta b/MCPForUnity/Editor/Clients/McpClientRegistry.cs.meta new file mode 100644 index 000000000..2e0400b55 --- /dev/null +++ b/MCPForUnity/Editor/Clients/McpClientRegistry.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4ce08555f995e4e848a826c63f18cb35 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Clients/TraeConfigurator.cs b/MCPForUnity/Editor/Clients/TraeConfigurator.cs new file mode 100644 index 000000000..2b35142ce --- /dev/null +++ b/MCPForUnity/Editor/Clients/TraeConfigurator.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.IO; +using MCPForUnity.Editor.Models; + +namespace MCPForUnity.Editor.Clients +{ + public class TraeConfigurator : JsonFileMcpConfigurator + { + public TraeConfigurator() : base(new McpClient + { + name = "Trae", + windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Trae", "mcp.json"), + macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Trae", "mcp.json"), + linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Trae", "mcp.json"), + }) + { } + + public override IList GetInstallationSteps() => new List + { + "Open Trae > Settings > MCP > Add Manually", + "Paste JSON or point to mcp.json", + "Save and restart" + }; + } +} diff --git a/MCPForUnity/Editor/Clients/TraeConfigurator.cs.meta b/MCPForUnity/Editor/Clients/TraeConfigurator.cs.meta new file mode 100644 index 000000000..09e953c11 --- /dev/null +++ b/MCPForUnity/Editor/Clients/TraeConfigurator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b3ab39e22ae0948ab94beae307f9902e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Clients/VSCodeConfigurator.cs b/MCPForUnity/Editor/Clients/VSCodeConfigurator.cs new file mode 100644 index 000000000..ac3ad9055 --- /dev/null +++ b/MCPForUnity/Editor/Clients/VSCodeConfigurator.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.IO; +using MCPForUnity.Editor.Models; + +namespace MCPForUnity.Editor.Clients +{ + public class VSCodeConfigurator : JsonFileMcpConfigurator + { + public VSCodeConfigurator() : base(new McpClient + { + name = "VSCode GitHub Copilot", + windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Code", "User", "mcp.json"), + macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Code", "User", "mcp.json"), + linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Code", "User", "mcp.json"), + IsVsCodeLayout = true + }) + { } + + public override IList GetInstallationSteps() => new List + { + "Install GitHub Copilot extension", + "Open/Create mcp.json at the path", + "Paste JSON", + "Save and restart VSCode" + }; + } +} diff --git a/MCPForUnity/Editor/Clients/VSCodeConfigurator.cs.meta b/MCPForUnity/Editor/Clients/VSCodeConfigurator.cs.meta new file mode 100644 index 000000000..056d0d412 --- /dev/null +++ b/MCPForUnity/Editor/Clients/VSCodeConfigurator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bcc7ead475a4d4ea2978151c217757b8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Clients/WindsurfConfigurator.cs b/MCPForUnity/Editor/Clients/WindsurfConfigurator.cs new file mode 100644 index 000000000..83fb94214 --- /dev/null +++ b/MCPForUnity/Editor/Clients/WindsurfConfigurator.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.IO; +using MCPForUnity.Editor.Models; + +namespace MCPForUnity.Editor.Clients +{ + public class WindsurfConfigurator : JsonFileMcpConfigurator + { + public WindsurfConfigurator() : base(new McpClient + { + name = "Windsurf", + windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codeium", "windsurf", "mcp_config.json"), + macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codeium", "windsurf", "mcp_config.json"), + linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codeium", "windsurf", "mcp_config.json"), + HttpUrlProperty = "serverUrl", + DefaultUnityFields = { { "disabled", false } }, + StripEnvWhenNotRequired = true + }) + { } + + public override IList GetInstallationSteps() => new List + { + "Open Windsurf", + "Settings > MCP > Manage MCPs > View raw config", + "Paste JSON", + "Save and restart" + }; + + public override string GetManualSnippet() + { + // Force consistent handling for Windsurf; reuse base behavior + return base.GetManualSnippet(); + } + } +} diff --git a/MCPForUnity/Editor/Clients/WindsurfConfigurator.cs.meta b/MCPForUnity/Editor/Clients/WindsurfConfigurator.cs.meta new file mode 100644 index 000000000..1b9515663 --- /dev/null +++ b/MCPForUnity/Editor/Clients/WindsurfConfigurator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b528971e189f141d38db577f155bd222 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Data/McpClients.cs b/MCPForUnity/Editor/Data/McpClients.cs deleted file mode 100644 index 687175105..000000000 --- a/MCPForUnity/Editor/Data/McpClients.cs +++ /dev/null @@ -1,225 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using MCPForUnity.Editor.Models; - -namespace MCPForUnity.Editor.Data -{ - public class McpClients - { - public List clients = new() - { - // 1) Cursor - new() - { - name = "Cursor", - windowsConfigPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".cursor", - "mcp.json" - ), - macConfigPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".cursor", - "mcp.json" - ), - linuxConfigPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".cursor", - "mcp.json" - ), - mcpType = McpTypes.Cursor, - configStatus = "Not Configured", - }, - // 2) Claude Code - new() - { - name = "Claude Code", - windowsConfigPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".claude.json" - ), - macConfigPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".claude.json" - ), - linuxConfigPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".claude.json" - ), - mcpType = McpTypes.ClaudeCode, - configStatus = "Not Configured", - }, - // 3) Windsurf - new() - { - name = "Windsurf", - windowsConfigPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".codeium", - "windsurf", - "mcp_config.json" - ), - macConfigPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".codeium", - "windsurf", - "mcp_config.json" - ), - linuxConfigPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".codeium", - "windsurf", - "mcp_config.json" - ), - mcpType = McpTypes.Windsurf, - configStatus = "Not Configured", - }, - // 4) Claude Desktop - new() - { - name = "Claude Desktop", - windowsConfigPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "Claude", - "claude_desktop_config.json" - ), - - macConfigPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - "Library", - "Application Support", - "Claude", - "claude_desktop_config.json" - ), - linuxConfigPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".config", - "Claude", - "claude_desktop_config.json" - ), - - mcpType = McpTypes.ClaudeDesktop, - configStatus = "Not Configured", - }, - // 5) VSCode GitHub Copilot - new() - { - name = "VSCode GitHub Copilot", - // Windows path is canonical under %AppData%\Code\User - windowsConfigPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "Code", - "User", - "mcp.json" - ), - // macOS: ~/Library/Application Support/Code/User/mcp.json - macConfigPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - "Library", - "Application Support", - "Code", - "User", - "mcp.json" - ), - // Linux: ~/.config/Code/User/mcp.json - linuxConfigPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".config", - "Code", - "User", - "mcp.json" - ), - mcpType = McpTypes.VSCode, - configStatus = "Not Configured", - }, - // Trae IDE - new() - { - name = "Trae", - // Windows: %AppData%\Trae\mcp.json - windowsConfigPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "Trae", - "mcp.json" - ), - // macOS: ~/Library/Application Support/Trae/mcp.json - macConfigPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - "Library", - "Application Support", - "Trae", - "mcp.json" - ), - // Linux: ~/.config/Trae/mcp.json - linuxConfigPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".config", - "Trae", - "mcp.json" - ), - mcpType = McpTypes.Trae, - configStatus = "Not Configured", - }, - // 3) Kiro - new() - { - name = "Kiro", - windowsConfigPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".kiro", - "settings", - "mcp.json" - ), - macConfigPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".kiro", - "settings", - "mcp.json" - ), - linuxConfigPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".kiro", - "settings", - "mcp.json" - ), - mcpType = McpTypes.Kiro, - configStatus = "Not Configured", - }, - // 4) Codex CLI - new() - { - name = "Codex CLI", - windowsConfigPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".codex", - "config.toml" - ), - macConfigPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".codex", - "config.toml" - ), - linuxConfigPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".codex", - "config.toml" - ), - mcpType = McpTypes.Codex, - configStatus = "Not Configured", - }, - }; - - // Initialize status enums after construction - public McpClients() - { - foreach (var client in clients) - { - if (client.configStatus == "Not Configured") - { - client.status = McpStatus.NotConfigured; - } - } - } - } -} diff --git a/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs b/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs index 084e2a7ea..294579b49 100644 --- a/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs +++ b/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using System; using System.Collections.Generic; using Newtonsoft.Json.Linq; using MCPForUnity.Editor.Models; @@ -13,16 +14,8 @@ public static class ConfigJsonBuilder public static string BuildManualConfigJson(string uvPath, McpClient client) { var root = new JObject(); - bool isVSCode = client?.mcpType == McpTypes.VSCode; - JObject container; - if (isVSCode) - { - container = EnsureObject(root, "servers"); - } - else - { - container = EnsureObject(root, "mcpServers"); - } + bool isVSCode = client?.IsVsCodeLayout == true; + JObject container = isVSCode ? EnsureObject(root, "servers") : EnsureObject(root, "mcpServers"); var unity = new JObject(); PopulateUnityNode(unity, uvPath, client, isVSCode); @@ -35,7 +28,7 @@ public static string BuildManualConfigJson(string uvPath, McpClient client) public static JObject ApplyUnityServerToExistingConfig(JObject root, string uvPath, McpClient client) { if (root == null) root = new JObject(); - bool isVSCode = client?.mcpType == McpTypes.VSCode; + bool isVSCode = client?.IsVsCodeLayout == true; JObject container = isVSCode ? EnsureObject(root, "servers") : EnsureObject(root, "mcpServers"); JObject unity = container["unityMCP"] as JObject ?? new JObject(); PopulateUnityNode(unity, uvPath, client, isVSCode); @@ -54,21 +47,20 @@ public static JObject ApplyUnityServerToExistingConfig(JObject root, string uvPa private static void PopulateUnityNode(JObject unity, string uvPath, McpClient client, bool isVSCode) { // Get transport preference (default to HTTP) - bool useHttpTransport = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true); - bool isWindsurf = client?.mcpType == McpTypes.Windsurf; + bool useHttpTransport = client?.SupportsHttpTransport != false && EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true); + string httpProperty = string.IsNullOrEmpty(client?.HttpUrlProperty) ? "url" : client.HttpUrlProperty; + var urlPropsToRemove = new HashSet(StringComparer.OrdinalIgnoreCase) { "url", "serverUrl" }; + urlPropsToRemove.Remove(httpProperty); if (useHttpTransport) { // HTTP mode: Use URL, no command string httpUrl = HttpEndpointUtility.GetMcpRpcUrl(); - string httpProperty = isWindsurf ? "serverUrl" : "url"; unity[httpProperty] = httpUrl; - // Remove legacy property for Windsurf (or vice versa) - string staleProperty = isWindsurf ? "url" : "serverUrl"; - if (unity[staleProperty] != null) + foreach (var prop in urlPropsToRemove) { - unity.Remove(staleProperty); + if (unity[prop] != null) unity.Remove(prop); } // Remove command/args if they exist from previous config @@ -102,6 +94,10 @@ private static void PopulateUnityNode(JObject unity, string uvPath, McpClient cl // Remove url/serverUrl if they exist from previous config if (unity["url"] != null) unity.Remove("url"); if (unity["serverUrl"] != null) unity.Remove("serverUrl"); + foreach (var prop in urlPropsToRemove) + { + if (unity[prop] != null) unity.Remove(prop); + } if (isVSCode) { @@ -115,8 +111,8 @@ private static void PopulateUnityNode(JObject unity, string uvPath, McpClient cl unity.Remove("type"); } - bool requiresEnv = client?.mcpType == McpTypes.Kiro; - bool requiresDisabled = client != null && (client.mcpType == McpTypes.Windsurf || client.mcpType == McpTypes.Kiro); + bool requiresEnv = client?.EnsureEnvObject == true; + bool stripEnv = client?.StripEnvWhenNotRequired == true; if (requiresEnv) { @@ -125,14 +121,20 @@ private static void PopulateUnityNode(JObject unity, string uvPath, McpClient cl unity["env"] = new JObject(); } } - else if (isWindsurf && unity["env"] != null) + else if (stripEnv && unity["env"] != null) { unity.Remove("env"); } - if (requiresDisabled && unity["disabled"] == null) + if (client?.DefaultUnityFields != null) { - unity["disabled"] = false; + foreach (var kvp in client.DefaultUnityFields) + { + if (unity[kvp.Key] == null) + { + unity[kvp.Key] = kvp.Value != null ? JToken.FromObject(kvp.Value) : JValue.CreateNull(); + } + } } } diff --git a/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs b/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs index 2552f9a25..9ead6f2aa 100644 --- a/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs +++ b/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs @@ -79,7 +79,7 @@ public static string WriteMcpConfiguration(string configPath, McpClient mcpClien // Determine existing entry references (command/args) string existingCommand = null; string[] existingArgs = null; - bool isVSCode = (mcpClient?.mcpType == McpTypes.VSCode); + bool isVSCode = (mcpClient?.IsVsCodeLayout == true); try { if (isVSCode) diff --git a/MCPForUnity/Editor/Migrations/StdIoVersionMigration.cs b/MCPForUnity/Editor/Migrations/StdIoVersionMigration.cs index 9f43734ff..b00a4faf9 100644 --- a/MCPForUnity/Editor/Migrations/StdIoVersionMigration.cs +++ b/MCPForUnity/Editor/Migrations/StdIoVersionMigration.cs @@ -1,6 +1,6 @@ using System; using System.IO; -using MCPForUnity.Editor.Data; +using MCPForUnity.Editor.Clients; using MCPForUnity.Editor.Helpers; using MCPForUnity.Editor.Models; using MCPForUnity.Editor.Services; @@ -8,6 +8,7 @@ using UnityEditor; using UnityEngine; using MCPForUnity.Editor.Constants; +using System.Linq; namespace MCPForUnity.Editor.Migrations { @@ -48,21 +49,24 @@ private static void RunMigrationIfNeeded() bool hadFailures = false; bool touchedAny = false; - var clients = new McpClients().clients; - foreach (var client in clients) + var configurators = McpClientRegistry.All.OfType().ToList(); + foreach (var configurator in configurators) { try { - if (!ConfigUsesStdIo(client)) + if (!ConfigUsesStdIo(configurator.Client)) continue; - MCPServiceLocator.Client.ConfigureClient(client); + if (!configurator.SupportsAutoConfigure) + continue; + + MCPServiceLocator.Client.ConfigureClient(configurator); touchedAny = true; } catch (Exception ex) { hadFailures = true; - McpLog.Warn($"Failed to refresh stdio config for {client.name}: {ex.Message}"); + McpLog.Warn($"Failed to refresh stdio config for {configurator.DisplayName}: {ex.Message}"); } } @@ -90,13 +94,7 @@ private static void RunMigrationIfNeeded() private static bool ConfigUsesStdIo(McpClient client) { - switch (client.mcpType) - { - case McpTypes.Codex: - return CodexConfigUsesStdIo(client); - default: - return JsonConfigUsesStdIo(client); - } + return JsonConfigUsesStdIo(client); } private static bool JsonConfigUsesStdIo(McpClient client) @@ -112,7 +110,7 @@ private static bool JsonConfigUsesStdIo(McpClient client) var root = JObject.Parse(File.ReadAllText(configPath)); JToken unityNode = null; - if (client.mcpType == McpTypes.VSCode) + if (client.IsVsCodeLayout) { unityNode = root.SelectToken("servers.unityMCP") ?? root.SelectToken("mcp.servers.unityMCP"); @@ -132,24 +130,5 @@ private static bool JsonConfigUsesStdIo(McpClient client) } } - private static bool CodexConfigUsesStdIo(McpClient client) - { - try - { - string configPath = McpConfigurationHelper.GetClientConfigPath(client); - if (string.IsNullOrEmpty(configPath) || !File.Exists(configPath)) - { - return false; - } - - string toml = File.ReadAllText(configPath); - return CodexConfigHelper.TryParseCodexServer(toml, out var command, out _) - && !string.IsNullOrEmpty(command); - } - catch - { - return false; - } - } } } diff --git a/MCPForUnity/Editor/Models/McpClient.cs b/MCPForUnity/Editor/Models/McpClient.cs index a32f7f596..480c021e0 100644 --- a/MCPForUnity/Editor/Models/McpClient.cs +++ b/MCPForUnity/Editor/Models/McpClient.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; + namespace MCPForUnity.Editor.Models { public class McpClient @@ -6,10 +8,17 @@ public class McpClient public string windowsConfigPath; public string macConfigPath; public string linuxConfigPath; - public McpTypes mcpType; public string configStatus; public McpStatus status = McpStatus.NotConfigured; + // Capability flags/config for JSON-based configurators + public bool IsVsCodeLayout; + public bool SupportsHttpTransport = true; + public bool EnsureEnvObject; + public bool StripEnvWhenNotRequired; + public string HttpUrlProperty = "url"; + public Dictionary DefaultUnityFields = new(); + // Helper method to convert the enum to a display string public string GetStatusDisplayString() { diff --git a/MCPForUnity/Editor/Models/McpTypes.cs b/MCPForUnity/Editor/Models/McpTypes.cs deleted file mode 100644 index 7c8648979..000000000 --- a/MCPForUnity/Editor/Models/McpTypes.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace MCPForUnity.Editor.Models -{ - public enum McpTypes - { - ClaudeCode, - ClaudeDesktop, - Codex, - Cursor, - Kiro, - VSCode, - Windsurf, - Trae, - } -} diff --git a/MCPForUnity/Editor/Services/ClientConfigurationService.cs b/MCPForUnity/Editor/Services/ClientConfigurationService.cs index 546ea38e8..3c48abdf5 100644 --- a/MCPForUnity/Editor/Services/ClientConfigurationService.cs +++ b/MCPForUnity/Editor/Services/ClientConfigurationService.cs @@ -1,15 +1,8 @@ using System; -using System.IO; +using System.Collections.Generic; using System.Linq; -using System.Runtime.InteropServices; -using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Data; -using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Clients; using MCPForUnity.Editor.Models; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using UnityEditor; -using UnityEngine; namespace MCPForUnity.Editor.Services { @@ -18,565 +11,49 @@ namespace MCPForUnity.Editor.Services /// public class ClientConfigurationService : IClientConfigurationService { - private readonly Data.McpClients mcpClients = new(); + private readonly List configurators; - public void ConfigureClient(McpClient client) + public ClientConfigurationService() { - var pathService = MCPServiceLocator.Paths; - string uvxPath = pathService.GetUvxPath(); - - string configPath = McpConfigurationHelper.GetClientConfigPath(client); - McpConfigurationHelper.EnsureConfigDirectoryExists(configPath); + configurators = McpClientRegistry.All.ToList(); + } - string result = client.mcpType == McpTypes.Codex - ? McpConfigurationHelper.ConfigureCodexClient(configPath, client) - : McpConfigurationHelper.WriteMcpConfiguration(configPath, client); + public IReadOnlyList GetAllClients() => configurators; - if (result == "Configured successfully") - { - client.SetStatus(McpStatus.Configured); - } - else - { - client.SetStatus(McpStatus.NotConfigured); - throw new InvalidOperationException($"Configuration failed: {result}"); - } + public void ConfigureClient(IMcpClientConfigurator configurator) + { + configurator.Configure(); } public ClientConfigurationSummary ConfigureAllDetectedClients() { var summary = new ClientConfigurationSummary(); - var pathService = MCPServiceLocator.Paths; - - foreach (var client in mcpClients.clients) + foreach (var configurator in configurators) { try { // Always re-run configuration so core fields stay current - CheckClientStatus(client, attemptAutoRewrite: false); - - // Check if required tools are available - if (client.mcpType == McpTypes.ClaudeCode) - { - if (!pathService.IsClaudeCliDetected()) - { - summary.SkippedCount++; - summary.Messages.Add($"➜ {client.name}: Claude CLI not found"); - continue; - } - - // Force a fresh registration so transport settings stay current - UnregisterClaudeCode(); - RegisterClaudeCode(); - summary.SuccessCount++; - summary.Messages.Add($"✓ {client.name}: Re-registered successfully"); - } - else - { - ConfigureClient(client); - summary.SuccessCount++; - summary.Messages.Add($"✓ {client.name}: Configured successfully"); - } + configurator.CheckStatus(attemptAutoRewrite: false); + configurator.Configure(); + summary.SuccessCount++; + summary.Messages.Add($"✓ {configurator.DisplayName}: Configured successfully"); } catch (Exception ex) { summary.FailureCount++; - summary.Messages.Add($"⚠ {client.name}: {ex.Message}"); + summary.Messages.Add($"⚠ {configurator.DisplayName}: {ex.Message}"); } } return summary; } - public bool CheckClientStatus(McpClient client, bool attemptAutoRewrite = true) + public bool CheckClientStatus(IMcpClientConfigurator configurator, bool attemptAutoRewrite = true) { - var previousStatus = client.status; - - try - { - // Special handling for Claude Code - if (client.mcpType == McpTypes.ClaudeCode) - { - CheckClaudeCodeConfiguration(client); - return client.status != previousStatus; - } - - string configPath = McpConfigurationHelper.GetClientConfigPath(client); - - if (!File.Exists(configPath)) - { - client.SetStatus(McpStatus.NotConfigured); - return client.status != previousStatus; - } - - string configJson = File.ReadAllText(configPath); - // Check configuration based on client type - string[] args = null; - string configuredUrl = null; - bool configExists = false; - - switch (client.mcpType) - { - case McpTypes.VSCode: - var vsConfig = JsonConvert.DeserializeObject(configJson) as JObject; - if (vsConfig != null) - { - var unityToken = - vsConfig["servers"]?["unityMCP"] - ?? vsConfig["mcp"]?["servers"]?["unityMCP"]; - - if (unityToken is JObject unityObj) - { - configExists = true; - - var argsToken = unityObj["args"]; - if (argsToken is JArray) - { - args = argsToken.ToObject(); - } - - var urlToken = unityObj["url"] ?? unityObj["serverUrl"]; - if (urlToken != null && urlToken.Type != JTokenType.Null) - { - configuredUrl = urlToken.ToString(); - } - } - } - break; - - case McpTypes.Codex: - if (CodexConfigHelper.TryParseCodexServer(configJson, out _, out var codexArgs, out var codexUrl)) - { - args = codexArgs; - configuredUrl = codexUrl; - configExists = true; - } - break; - - default: - McpConfig standardConfig = JsonConvert.DeserializeObject(configJson); - if (standardConfig?.mcpServers?.unityMCP != null) - { - args = standardConfig.mcpServers.unityMCP.args; - configExists = true; - } - break; - } - - if (configExists) - { - bool matches = false; - - if (args != null && args.Length > 0) - { - string expectedUvxUrl = AssetPathUtility.GetMcpServerGitUrl(); - string configuredUvxUrl = McpConfigurationHelper.ExtractUvxUrl(args); - matches = !string.IsNullOrEmpty(configuredUvxUrl) && - McpConfigurationHelper.PathsEqual(configuredUvxUrl, expectedUvxUrl); - } - else if (!string.IsNullOrEmpty(configuredUrl)) - { - string expectedUrl = HttpEndpointUtility.GetMcpRpcUrl(); - matches = UrlsEqual(configuredUrl, expectedUrl); - } - - if (matches) - { - client.SetStatus(McpStatus.Configured); - } - else if (attemptAutoRewrite) - { - // Attempt auto-rewrite if path mismatch detected - try - { - string rewriteResult = client.mcpType == McpTypes.Codex - ? McpConfigurationHelper.ConfigureCodexClient(configPath, client) - : McpConfigurationHelper.WriteMcpConfiguration(configPath, client); - - if (rewriteResult == "Configured successfully") - { - bool debugLogsEnabled = EditorPrefs.GetBool(EditorPrefKeys.DebugLogs, false); - if (debugLogsEnabled) - { - string targetDescriptor = args != null && args.Length > 0 - ? AssetPathUtility.GetMcpServerGitUrl() - : HttpEndpointUtility.GetMcpRpcUrl(); - McpLog.Info($"Auto-updated MCP config for '{client.name}' to new version: {targetDescriptor}", always: false); - } - client.SetStatus(McpStatus.Configured); - } - else - { - client.SetStatus(McpStatus.IncorrectPath); - } - } - catch - { - client.SetStatus(McpStatus.IncorrectPath); - } - } - else - { - client.SetStatus(McpStatus.IncorrectPath); - } - } - else - { - client.SetStatus(McpStatus.MissingConfig); - } - } - catch (Exception ex) - { - client.SetStatus(McpStatus.Error, ex.Message); - } - - return client.status != previousStatus; + var previous = configurator.Status; + var current = configurator.CheckStatus(attemptAutoRewrite); + return current != previous; } - public void RegisterClaudeCode() - { - var pathService = MCPServiceLocator.Paths; - string claudePath = pathService.GetClaudeCliPath(); - if (string.IsNullOrEmpty(claudePath)) - { - throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first."); - } - - // Check transport preference - bool useHttpTransport = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true); - - string args; - if (useHttpTransport) - { - // HTTP mode: Use --transport http with URL - string httpUrl = HttpEndpointUtility.GetMcpRpcUrl(); - args = $"mcp add --transport http UnityMCP {httpUrl}"; - } - else - { - // Stdio mode: Use command with uvx - var (uvxPath, gitUrl, packageName) = AssetPathUtility.GetUvxCommandParts(); - args = $"mcp add --transport stdio UnityMCP -- \"{uvxPath}\" --from \"{gitUrl}\" {packageName}"; - } - - string projectDir = Path.GetDirectoryName(Application.dataPath); - - string pathPrepend = null; - if (Application.platform == RuntimePlatform.OSXEditor) - { - pathPrepend = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"; - } - else if (Application.platform == RuntimePlatform.LinuxEditor) - { - pathPrepend = "/usr/local/bin:/usr/bin:/bin"; - } - - // Add the directory containing Claude CLI to PATH (for node/nvm scenarios) - try - { - string claudeDir = Path.GetDirectoryName(claudePath); - if (!string.IsNullOrEmpty(claudeDir)) - { - pathPrepend = string.IsNullOrEmpty(pathPrepend) - ? claudeDir - : $"{claudeDir}:{pathPrepend}"; - } - } - catch { } - - if (!ExecPath.TryRun(claudePath, args, projectDir, out var stdout, out var stderr, 15000, pathPrepend)) - { - string combined = ($"{stdout}\n{stderr}") ?? string.Empty; - if (combined.IndexOf("already exists", StringComparison.OrdinalIgnoreCase) >= 0) - { - McpLog.Info("MCP for Unity already registered with Claude Code."); - } - else - { - throw new InvalidOperationException($"Failed to register with Claude Code:\n{stderr}\n{stdout}"); - } - return; - } - - McpLog.Info("Successfully registered with Claude Code."); - - // Update status - var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode); - if (claudeClient != null) - { - CheckClaudeCodeConfiguration(claudeClient); - } - } - - public void UnregisterClaudeCode() - { - var pathService = MCPServiceLocator.Paths; - string claudePath = pathService.GetClaudeCliPath(); - - if (string.IsNullOrEmpty(claudePath)) - { - throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first."); - } - - string projectDir = Path.GetDirectoryName(Application.dataPath); - string pathPrepend = Application.platform == RuntimePlatform.OSXEditor - ? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin" - : null; - - // Check if UnityMCP server exists (fixed - only check for "UnityMCP") - bool serverExists = ExecPath.TryRun(claudePath, "mcp get UnityMCP", projectDir, out _, out _, 7000, pathPrepend); - - if (!serverExists) - { - // Nothing to unregister - var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode); - if (claudeClient != null) - { - claudeClient.SetStatus(McpStatus.NotConfigured); - } - McpLog.Info("No MCP for Unity server found - already unregistered."); - return; - } - - // Remove the server - if (ExecPath.TryRun(claudePath, "mcp remove UnityMCP", projectDir, out var stdout, out var stderr, 10000, pathPrepend)) - { - McpLog.Info("MCP server successfully unregistered from Claude Code."); - } - else - { - throw new InvalidOperationException($"Failed to unregister: {stderr}"); - } - - // Update status - var client = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode); - if (client != null) - { - client.SetStatus(McpStatus.NotConfigured); - CheckClaudeCodeConfiguration(client); - } - } - - public string GetConfigPath(McpClient client) - { - // Claude Code is managed via CLI, not config files - if (client.mcpType == McpTypes.ClaudeCode) - { - return "Not applicable (managed via Claude CLI)"; - } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - return client.windowsConfigPath; - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - return client.macConfigPath; - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - return client.linuxConfigPath; - - return "Unknown"; - } - - public string GenerateConfigJson(McpClient client) - { - string uvxPath = MCPServiceLocator.Paths.GetUvxPath(); - - // Claude Code uses CLI commands, not JSON config - if (client.mcpType == McpTypes.ClaudeCode) - { - // Check transport preference - bool useHttpTransport = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true); - - string registerCommand; - if (useHttpTransport) - { - // HTTP mode - string httpUrl = HttpEndpointUtility.GetMcpRpcUrl(); - registerCommand = $"claude mcp add --transport http UnityMCP {httpUrl}"; - } - else - { - // Stdio mode - if (string.IsNullOrEmpty(uvxPath)) - { - return "# Error: Configuration not available - check paths in Advanced Settings"; - } - - string gitUrl = AssetPathUtility.GetMcpServerGitUrl(); - registerCommand = $"claude mcp add --transport stdio UnityMCP -- \"{uvxPath}\" --from \"{gitUrl}\" mcp-for-unity"; - } - - return "# Register the MCP server with Claude Code:\n" + - $"{registerCommand}\n\n" + - "# Unregister the MCP server:\n" + - "claude mcp remove UnityMCP\n\n" + - "# List registered servers:\n" + - "claude mcp list # Only works when claude is run in the project's directory"; - } - - if (string.IsNullOrEmpty(uvxPath)) - return "{ \"error\": \"Configuration not available - check paths in Advanced Settings\" }"; - - try - { - if (client.mcpType == McpTypes.Codex) - { - return CodexConfigHelper.BuildCodexServerBlock(uvxPath); - } - else - { - return ConfigJsonBuilder.BuildManualConfigJson(uvxPath, client); - } - } - catch (Exception ex) - { - return $"{{ \"error\": \"{ex.Message}\" }}"; - } - } - - public string GetInstallationSteps(McpClient client) - { - string baseSteps = client.mcpType switch - { - McpTypes.ClaudeDesktop => - "1. Open Claude Desktop\n" + - "2. Go to Settings > Developer > Edit Config\n" + - " OR open the config file at the path above\n" + - "3. Paste the configuration JSON\n" + - "4. Save and restart Claude Desktop", - - McpTypes.Cursor => - "1. Open Cursor\n" + - "2. Go to File > Preferences > Cursor Settings > MCP > Add new global MCP server\n" + - " OR open the config file at the path above\n" + - "3. Paste the configuration JSON\n" + - "4. Save and restart Cursor", - - McpTypes.Windsurf => - "1. Open Windsurf\n" + - "2. Go to File > Preferences > Windsurf Settings > MCP > Manage MCPs > View raw config\n" + - " OR open the config file at the path above\n" + - "3. Paste the configuration JSON\n" + - "4. Save and restart Windsurf", - - McpTypes.VSCode => - "1. Ensure VSCode and GitHub Copilot extension are installed\n" + - "2. Open or create mcp.json at the path above\n" + - "3. Paste the configuration JSON\n" + - "4. Save and restart VSCode", - - McpTypes.Kiro => - "1. Open Kiro\n" + - "2. Go to File > Settings > Settings > Search for \"MCP\" > Open Workspace MCP Config\n" + - " OR open the config file at the path above\n" + - "3. Paste the configuration JSON\n" + - "4. Save and restart Kiro", - - McpTypes.Codex => - "1. Run 'codex config edit' in a terminal\n" + - " OR open the config file at the path above\n" + - "2. Paste the configuration TOML\n" + - "3. Save and restart Codex", - - McpTypes.ClaudeCode => - "1. Ensure Claude CLI is installed\n" + - "2. Use the Register button to register automatically\n" + - " OR manually run: claude mcp add UnityMCP\n" + - "3. Restart Claude Code", - - McpTypes.Trae => - "1. Open Trae and go to Settings > MCP\n" + - "2. Select Add Server > Add Manually\n" + - "3. Paste the JSON or point to the mcp.json file\n" + - " Windows: %AppData%\\Trae\\mcp.json\n" + - " macOS: ~/Library/Application Support/Trae/mcp.json\n" + - " Linux: ~/.config/Trae/mcp.json\n" + - "4. For local servers, Node.js (npx) or uvx must be installed\n" + - "5. Save and restart Trae", - - _ => "Configuration steps not available for this client." - }; - - return baseSteps; - } - - private void CheckClaudeCodeConfiguration(McpClient client) - { - try - { - var pathService = MCPServiceLocator.Paths; - string claudePath = pathService.GetClaudeCliPath(); - - if (string.IsNullOrEmpty(claudePath)) - { - client.SetStatus(McpStatus.NotConfigured, "Claude CLI not found"); - return; - } - - // Use 'claude mcp list' to check if UnityMCP is registered - string args = "mcp list"; - string projectDir = Path.GetDirectoryName(Application.dataPath); - - string pathPrepend = null; - if (Application.platform == RuntimePlatform.OSXEditor) - { - pathPrepend = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"; - } - else if (Application.platform == RuntimePlatform.LinuxEditor) - { - pathPrepend = "/usr/local/bin:/usr/bin:/bin"; - } - - // Add the directory containing Claude CLI to PATH - try - { - string claudeDir = Path.GetDirectoryName(claudePath); - if (!string.IsNullOrEmpty(claudeDir)) - { - pathPrepend = string.IsNullOrEmpty(pathPrepend) - ? claudeDir - : $"{claudeDir}:{pathPrepend}"; - } - } - catch { } - - if (ExecPath.TryRun(claudePath, args, projectDir, out var stdout, out var stderr, 10000, pathPrepend)) - { - // Check if UnityMCP is in the output - if (!string.IsNullOrEmpty(stdout) && stdout.IndexOf("UnityMCP", StringComparison.OrdinalIgnoreCase) >= 0) - { - client.SetStatus(McpStatus.Configured); - return; - } - } - - client.SetStatus(McpStatus.NotConfigured); - } - catch (Exception ex) - { - client.SetStatus(McpStatus.Error, ex.Message); - } - } - - private static bool UrlsEqual(string a, string b) - { - if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b)) - { - return false; - } - - if (Uri.TryCreate(a.Trim(), UriKind.Absolute, out var uriA) && - Uri.TryCreate(b.Trim(), UriKind.Absolute, out var uriB)) - { - return Uri.Compare( - uriA, - uriB, - UriComponents.HttpRequestUrl, - UriFormat.SafeUnescaped, - StringComparison.OrdinalIgnoreCase) == 0; - } - - string Normalize(string value) => value.Trim().TrimEnd('/'); - - return string.Equals(Normalize(a), Normalize(b), StringComparison.OrdinalIgnoreCase); - } } } diff --git a/MCPForUnity/Editor/Services/IClientConfigurationService.cs b/MCPForUnity/Editor/Services/IClientConfigurationService.cs index 24b01fad7..6172e8fb3 100644 --- a/MCPForUnity/Editor/Services/IClientConfigurationService.cs +++ b/MCPForUnity/Editor/Services/IClientConfigurationService.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; +using MCPForUnity.Editor.Clients; using MCPForUnity.Editor.Models; namespace MCPForUnity.Editor.Services @@ -11,7 +13,7 @@ public interface IClientConfigurationService /// Configures a specific MCP client /// /// The client to configure - void ConfigureClient(McpClient client); + void ConfigureClient(IMcpClientConfigurator configurator); /// /// Configures all detected/installed MCP clients (skips clients where CLI/tools not found) @@ -25,38 +27,10 @@ public interface IClientConfigurationService /// The client to check /// If true, attempts to auto-fix mismatched paths /// True if status changed, false otherwise - bool CheckClientStatus(McpClient client, bool attemptAutoRewrite = true); + bool CheckClientStatus(IMcpClientConfigurator configurator, bool attemptAutoRewrite = true); - /// - /// Registers MCP for Unity with Claude Code CLI - /// - void RegisterClaudeCode(); - - /// - /// Unregisters MCP for Unity from Claude Code CLI - /// - void UnregisterClaudeCode(); - - /// - /// Gets the configuration file path for a client - /// - /// The client - /// Platform-specific config path - string GetConfigPath(McpClient client); - - /// - /// Generates the configuration JSON for a client - /// - /// The client - /// JSON configuration string - string GenerateConfigJson(McpClient client); - - /// - /// Gets human-readable installation steps for a client - /// - /// The client - /// Installation instructions - string GetInstallationSteps(McpClient client); + /// Gets the registry of discovered configurators. + IReadOnlyList GetAllClients(); } /// diff --git a/MCPForUnity/Editor/Services/ServerManagementService.cs b/MCPForUnity/Editor/Services/ServerManagementService.cs index 8a323aba9..b4fe2f32e 100644 --- a/MCPForUnity/Editor/Services/ServerManagementService.cs +++ b/MCPForUnity/Editor/Services/ServerManagementService.cs @@ -2,7 +2,6 @@ using System.IO; using System.Linq; using MCPForUnity.Editor.Constants; -using MCPForUnity.Editor.Data; using MCPForUnity.Editor.Helpers; using UnityEditor; using UnityEngine; diff --git a/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs b/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs index 462ed4a2d..883526b41 100644 --- a/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs +++ b/MCPForUnity/Editor/Windows/Components/ClientConfig/McpClientConfigSection.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; @@ -6,7 +7,7 @@ using UnityEditor; using UnityEngine; using UnityEngine.UIElements; -using MCPForUnity.Editor.Data; +using MCPForUnity.Editor.Clients; using MCPForUnity.Editor.Helpers; using MCPForUnity.Editor.Models; using MCPForUnity.Editor.Services; @@ -36,15 +37,15 @@ public class McpClientConfigSection private Label installationStepsLabel; // Data - private readonly McpClients mcpClients; + private readonly List configurators; private int selectedClientIndex = 0; public VisualElement Root { get; private set; } - public McpClientConfigSection(VisualElement root, McpClients clients) + public McpClientConfigSection(VisualElement root) { Root = root; - mcpClients = clients; + configurators = MCPServiceLocator.Client.GetAllClients().ToList(); CacheUIElements(); InitializeUI(); RegisterCallbacks(); @@ -70,7 +71,7 @@ private void CacheUIElements() private void InitializeUI() { - var clientNames = mcpClients.clients.Select(c => c.name).ToList(); + var clientNames = configurators.Select(c => c.DisplayName).ToList(); clientDropdown.choices = clientNames; if (clientNames.Count > 0) { @@ -100,20 +101,20 @@ private void RegisterCallbacks() public void UpdateClientStatus() { - if (selectedClientIndex < 0 || selectedClientIndex >= mcpClients.clients.Count) + if (selectedClientIndex < 0 || selectedClientIndex >= configurators.Count) return; - var client = mcpClients.clients[selectedClientIndex]; + var client = configurators[selectedClientIndex]; MCPServiceLocator.Client.CheckClientStatus(client); - clientStatusLabel.text = client.GetStatusDisplayString(); + clientStatusLabel.text = GetStatusDisplayString(client.Status); clientStatusLabel.style.color = StyleKeyword.Null; clientStatusIndicator.RemoveFromClassList("configured"); clientStatusIndicator.RemoveFromClassList("not-configured"); clientStatusIndicator.RemoveFromClassList("warning"); - switch (client.status) + switch (client.Status) { case McpStatus.Configured: case McpStatus.Running: @@ -130,42 +131,60 @@ public void UpdateClientStatus() break; } - if (client.mcpType == McpTypes.ClaudeCode) - { - bool isConfigured = client.status == McpStatus.Configured; - configureButton.text = isConfigured ? "Unregister" : "Register"; - } - else + configureButton.text = client.GetConfigureActionLabel(); + } + + private string GetStatusDisplayString(McpStatus status) + { + return status switch { - configureButton.text = "Configure"; - } + McpStatus.NotConfigured => "Not Configured", + McpStatus.Configured => "Configured", + McpStatus.Running => "Running", + McpStatus.Connected => "Connected", + McpStatus.IncorrectPath => "Incorrect Path", + McpStatus.CommunicationError => "Communication Error", + McpStatus.NoResponse => "No Response", + McpStatus.UnsupportedOS => "Unsupported OS", + McpStatus.MissingConfig => "Missing MCPForUnity Config", + McpStatus.Error => "Error", + _ => "Unknown", + }; } public void UpdateManualConfiguration() { - if (selectedClientIndex < 0 || selectedClientIndex >= mcpClients.clients.Count) + if (selectedClientIndex < 0 || selectedClientIndex >= configurators.Count) return; - var client = mcpClients.clients[selectedClientIndex]; + var client = configurators[selectedClientIndex]; - string configPath = MCPServiceLocator.Client.GetConfigPath(client); + string configPath = client.GetConfigPath(); configPathField.value = configPath; - string configJson = MCPServiceLocator.Client.GenerateConfigJson(client); + string configJson = client.GetManualSnippet(); configJsonField.value = configJson; - string steps = MCPServiceLocator.Client.GetInstallationSteps(client); - installationStepsLabel.text = steps; + var steps = client.GetInstallationSteps(); + if (steps != null && steps.Count > 0) + { + var numbered = steps.Select((s, i) => $"{i + 1}. {s}"); + installationStepsLabel.text = string.Join("\n", numbered); + } + else + { + installationStepsLabel.text = "Configuration steps not available for this client."; + } } private void UpdateClaudeCliPathVisibility() { - if (selectedClientIndex < 0 || selectedClientIndex >= mcpClients.clients.Count) + if (selectedClientIndex < 0 || selectedClientIndex >= configurators.Count) return; - var client = mcpClients.clients[selectedClientIndex]; + var client = configurators[selectedClientIndex]; - if (client.mcpType == McpTypes.ClaudeCode) + if (client is ClaudeCliMcpConfigurator) { string claudePath = MCPServiceLocator.Paths.GetClaudeCliPath(); if (string.IsNullOrEmpty(claudePath)) @@ -199,7 +218,7 @@ private void OnConfigureAllClientsClicked() EditorUtility.DisplayDialog("Configure All Clients", message, "OK"); - if (selectedClientIndex >= 0 && selectedClientIndex < mcpClients.clients.Count) + if (selectedClientIndex >= 0 && selectedClientIndex < configurators.Count) { UpdateClientStatus(); UpdateManualConfiguration(); @@ -213,30 +232,14 @@ private void OnConfigureAllClientsClicked() private void OnConfigureClicked() { - if (selectedClientIndex < 0 || selectedClientIndex >= mcpClients.clients.Count) + if (selectedClientIndex < 0 || selectedClientIndex >= configurators.Count) return; - var client = mcpClients.clients[selectedClientIndex]; + var client = configurators[selectedClientIndex]; try { - if (client.mcpType == McpTypes.ClaudeCode) - { - bool isConfigured = client.status == McpStatus.Configured; - if (isConfigured) - { - MCPServiceLocator.Client.UnregisterClaudeCode(); - } - else - { - MCPServiceLocator.Client.RegisterClaudeCode(); - } - } - else - { - MCPServiceLocator.Client.ConfigureClient(client); - } - + MCPServiceLocator.Client.ConfigureClient(client); UpdateClientStatus(); UpdateManualConfiguration(); } @@ -308,9 +311,9 @@ private void OnCopyJsonClicked() public void RefreshSelectedClient() { - if (selectedClientIndex >= 0 && selectedClientIndex < mcpClients.clients.Count) + if (selectedClientIndex >= 0 && selectedClientIndex < configurators.Count) { - var client = mcpClients.clients[selectedClientIndex]; + var client = configurators[selectedClientIndex]; MCPServiceLocator.Client.CheckClientStatus(client); UpdateClientStatus(); UpdateManualConfiguration(); diff --git a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs index bef7dd7ae..926ce4cb1 100644 --- a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs +++ b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs @@ -4,9 +4,8 @@ using UnityEditor; using UnityEngine; using UnityEngine.UIElements; -using MCPForUnity.Editor.Data; -using MCPForUnity.Editor.Helpers; -using MCPForUnity.Editor.Services; +using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Services; using MCPForUnity.Editor.Windows.Components.Settings; using MCPForUnity.Editor.Windows.Components.Connection; using MCPForUnity.Editor.Windows.Components.ClientConfig; @@ -20,9 +19,7 @@ public class MCPForUnityEditorWindow : EditorWindow private McpConnectionSection connectionSection; private McpClientConfigSection clientConfigSection; - // Data - private readonly McpClients mcpClients = new(); - private static readonly HashSet OpenWindows = new(); + private static readonly HashSet OpenWindows = new(); public static void ShowWindow() { @@ -105,8 +102,8 @@ public void CreateGUI() { var clientConfigRoot = clientConfigTree.Instantiate(); sectionsContainer.Add(clientConfigRoot); - clientConfigSection = new McpClientConfigSection(clientConfigRoot, mcpClients); - } + clientConfigSection = new McpClientConfigSection(clientConfigRoot); + } // Initial updates RefreshAllData(); diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/WriteToConfigTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/WriteToConfigTests.cs index fff2740d2..65ebc879e 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/WriteToConfigTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/WriteToConfigTests.cs @@ -76,7 +76,13 @@ public void AddsDisabledFalseAndServerUrl_ForWindsurf() var configPath = Path.Combine(_tempRoot, "windsurf.json"); WriteInitialConfig(configPath, isVSCode: false, command: _fakeUvPath, directory: "/old/path"); - var client = new McpClient { name = "Windsurf", mcpType = McpTypes.Windsurf }; + var client = new McpClient + { + name = "Windsurf", + HttpUrlProperty = "serverUrl", + DefaultUnityFields = { { "disabled", false } }, + StripEnvWhenNotRequired = true + }; InvokeWriteToConfig(configPath, client); var root = JObject.Parse(File.ReadAllText(configPath)); @@ -84,7 +90,7 @@ public void AddsDisabledFalseAndServerUrl_ForWindsurf() Assert.NotNull(unity, "Expected mcpServers.unityMCP node"); Assert.IsNull(unity["env"], "Windsurf configs should not include an env block"); Assert.AreEqual(false, (bool)unity["disabled"], "disabled:false should be set for Windsurf when missing"); - AssertTransportConfiguration(unity, McpTypes.Windsurf); + AssertTransportConfiguration(unity, client); } [Test] @@ -93,7 +99,12 @@ public void AddsEnvAndDisabledFalse_ForKiro() var configPath = Path.Combine(_tempRoot, "kiro.json"); WriteInitialConfig(configPath, isVSCode: false, command: _fakeUvPath, directory: "/old/path"); - var client = new McpClient { name = "Kiro", mcpType = McpTypes.Kiro }; + var client = new McpClient + { + name = "Kiro", + EnsureEnvObject = true, + DefaultUnityFields = { { "disabled", false } } + }; InvokeWriteToConfig(configPath, client); var root = JObject.Parse(File.ReadAllText(configPath)); @@ -102,7 +113,7 @@ public void AddsEnvAndDisabledFalse_ForKiro() Assert.NotNull(unity["env"], "env should be present for all clients"); Assert.IsTrue(unity["env"]!.Type == JTokenType.Object, "env should be an object"); Assert.AreEqual(false, (bool)unity["disabled"], "disabled:false should be set for Kiro when missing"); - AssertTransportConfiguration(unity, McpTypes.Kiro); + AssertTransportConfiguration(unity, client); } [Test] @@ -111,7 +122,7 @@ public void DoesNotAddEnvOrDisabled_ForCursor() var configPath = Path.Combine(_tempRoot, "cursor.json"); WriteInitialConfig(configPath, isVSCode: false, command: _fakeUvPath, directory: "/old/path"); - var client = new McpClient { name = "Cursor", mcpType = McpTypes.Cursor }; + var client = new McpClient { name = "Cursor" }; InvokeWriteToConfig(configPath, client); var root = JObject.Parse(File.ReadAllText(configPath)); @@ -119,7 +130,7 @@ public void DoesNotAddEnvOrDisabled_ForCursor() Assert.NotNull(unity, "Expected mcpServers.unityMCP node"); Assert.IsNull(unity["env"], "env should not be added for non-Windsurf/Kiro clients"); Assert.IsNull(unity["disabled"], "disabled should not be added for non-Windsurf/Kiro clients"); - AssertTransportConfiguration(unity, McpTypes.Cursor); + AssertTransportConfiguration(unity, client); } [Test] @@ -128,7 +139,7 @@ public void DoesNotAddEnvOrDisabled_ForVSCode() var configPath = Path.Combine(_tempRoot, "vscode.json"); WriteInitialConfig(configPath, isVSCode: true, command: _fakeUvPath, directory: "/old/path"); - var client = new McpClient { name = "VSCode", mcpType = McpTypes.VSCode }; + var client = new McpClient { name = "VSCode", IsVsCodeLayout = true }; InvokeWriteToConfig(configPath, client); var root = JObject.Parse(File.ReadAllText(configPath)); @@ -136,7 +147,7 @@ public void DoesNotAddEnvOrDisabled_ForVSCode() Assert.NotNull(unity, "Expected servers.unityMCP node"); Assert.IsNull(unity["env"], "env should not be added for VSCode client"); Assert.IsNull(unity["disabled"], "disabled should not be added for VSCode client"); - AssertTransportConfiguration(unity, McpTypes.VSCode); + AssertTransportConfiguration(unity, client); } [Test] @@ -145,12 +156,7 @@ public void DoesNotAddEnvOrDisabled_ForTrae() var configPath = Path.Combine(_tempRoot, "trae.json"); WriteInitialConfig(configPath, isVSCode: false, command: _fakeUvPath, directory: "/old/path"); - if (!Enum.TryParse("Trae", out var traeValue)) - { - Assert.Ignore("McpTypes.Trae not available in this package version; skipping test."); - } - - var client = new McpClient { name = "Trae", mcpType = traeValue }; + var client = new McpClient { name = "Trae" }; InvokeWriteToConfig(configPath, client); var root = JObject.Parse(File.ReadAllText(configPath)); @@ -158,7 +164,7 @@ public void DoesNotAddEnvOrDisabled_ForTrae() Assert.NotNull(unity, "Expected mcpServers.unityMCP node"); Assert.IsNull(unity["env"], "env should not be added for Trae client"); Assert.IsNull(unity["disabled"], "disabled should not be added for Trae client"); - AssertTransportConfiguration(unity, traeValue); + AssertTransportConfiguration(unity, client); } [Test] @@ -182,7 +188,12 @@ public void PreservesExistingEnvAndDisabled_ForKiro() }; File.WriteAllText(configPath, json.ToString()); - var client = new McpClient { name = "Kiro", mcpType = McpTypes.Kiro }; + var client = new McpClient + { + name = "Kiro", + EnsureEnvObject = true, + DefaultUnityFields = { { "disabled", false } } + }; InvokeWriteToConfig(configPath, client); var root = JObject.Parse(File.ReadAllText(configPath)); @@ -190,7 +201,7 @@ public void PreservesExistingEnvAndDisabled_ForKiro() Assert.NotNull(unity, "Expected mcpServers.unityMCP node"); Assert.AreEqual("bar", (string)unity["env"]!["FOO"], "Existing env should be preserved"); Assert.AreEqual(true, (bool)unity["disabled"], "Existing disabled value should be preserved"); - AssertTransportConfiguration(unity, McpTypes.Kiro); + AssertTransportConfiguration(unity, client); } [Test] @@ -213,7 +224,13 @@ public void RemovesEnvBlock_ForWindsurf() }; File.WriteAllText(configPath, json.ToString()); - var client = new McpClient { name = "Windsurf", mcpType = McpTypes.Windsurf }; + var client = new McpClient + { + name = "Windsurf", + HttpUrlProperty = "serverUrl", + DefaultUnityFields = { { "disabled", false } }, + StripEnvWhenNotRequired = true + }; InvokeWriteToConfig(configPath, client); var root = JObject.Parse(File.ReadAllText(configPath)); @@ -221,7 +238,7 @@ public void RemovesEnvBlock_ForWindsurf() Assert.NotNull(unity, "Expected mcpServers.unityMCP node"); Assert.IsNull(unity["env"], "Windsurf config should strip any existing env block"); Assert.AreEqual(true, (bool)unity["disabled"], "Existing disabled value should be preserved"); - AssertTransportConfiguration(unity, McpTypes.Windsurf); + AssertTransportConfiguration(unity, client); } [Test] @@ -232,13 +249,19 @@ public void UsesStdioTransport_ForNonVSCodeClients_WhenPreferenceDisabled() WithTransportPreference(false, () => { - var client = new McpClient { name = "Windsurf", mcpType = McpTypes.Windsurf }; + var client = new McpClient + { + name = "Windsurf", + HttpUrlProperty = "serverUrl", + DefaultUnityFields = { { "disabled", false } }, + StripEnvWhenNotRequired = true + }; InvokeWriteToConfig(configPath, client); var root = JObject.Parse(File.ReadAllText(configPath)); var unity = (JObject)root.SelectToken("mcpServers.unityMCP"); Assert.NotNull(unity, "Expected mcpServers.unityMCP node"); - AssertTransportConfiguration(unity, McpTypes.Windsurf); + AssertTransportConfiguration(unity, client); }); } @@ -250,13 +273,13 @@ public void UsesStdioTransport_ForVSCode_WhenPreferenceDisabled() WithTransportPreference(false, () => { - var client = new McpClient { name = "VSCode", mcpType = McpTypes.VSCode }; + var client = new McpClient { name = "VSCode", IsVsCodeLayout = true }; InvokeWriteToConfig(configPath, client); var root = JObject.Parse(File.ReadAllText(configPath)); var unity = (JObject)root.SelectToken("servers.unityMCP"); Assert.NotNull(unity, "Expected servers.unityMCP node"); - AssertTransportConfiguration(unity, McpTypes.VSCode); + AssertTransportConfiguration(unity, client); }); } @@ -324,11 +347,11 @@ private static void InvokeWriteToConfig(string configPath, McpClient client) Assert.AreEqual("Configured successfully", result, "WriteMcpConfiguration should return success"); } - private static void AssertTransportConfiguration(JObject unity, McpTypes clientType) + private static void AssertTransportConfiguration(JObject unity, McpClient client) { bool useHttp = EditorPrefs.GetBool(UseHttpTransportPrefKey, true); - bool isVSCode = clientType == McpTypes.VSCode; - bool isWindsurf = clientType == McpTypes.Windsurf; + bool isVSCode = client.IsVsCodeLayout; + bool isWindsurf = string.Equals(client.HttpUrlProperty, "serverUrl", StringComparison.OrdinalIgnoreCase); if (useHttp) { From 5ece8d415bc38577213c44d656549f40722e8123 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Thu, 27 Nov 2025 01:15:24 -0400 Subject: [PATCH 02/14] Restore original text instructions Well most of them, I modified a few --- MCPForUnity/Editor/Clients/ClaudeCodeConfigurator.cs | 2 +- .../Editor/Clients/ClaudeDesktopConfigurator.cs | 6 +++--- MCPForUnity/Editor/Clients/CodexConfigurator.cs | 4 ++-- MCPForUnity/Editor/Clients/CursorConfigurator.cs | 6 +++--- MCPForUnity/Editor/Clients/KiroConfigurator.cs | 6 +++--- MCPForUnity/Editor/Clients/TraeConfigurator.cs | 10 +++++++--- MCPForUnity/Editor/Clients/VSCodeConfigurator.cs | 4 ++-- MCPForUnity/Editor/Clients/WindsurfConfigurator.cs | 6 +++--- 8 files changed, 24 insertions(+), 20 deletions(-) diff --git a/MCPForUnity/Editor/Clients/ClaudeCodeConfigurator.cs b/MCPForUnity/Editor/Clients/ClaudeCodeConfigurator.cs index 215c2dca3..056e1ac9a 100644 --- a/MCPForUnity/Editor/Clients/ClaudeCodeConfigurator.cs +++ b/MCPForUnity/Editor/Clients/ClaudeCodeConfigurator.cs @@ -17,7 +17,7 @@ public ClaudeCodeConfigurator() : base(new McpClient public override IList GetInstallationSteps() => new List { "Ensure Claude CLI is installed", - "Use Register to add UnityMCP (or run claude mcp add UnityMCP)", + "Use the Register button to register automatically\nOR manually run: claude mcp add UnityMCP", "Restart Claude Code" }; } diff --git a/MCPForUnity/Editor/Clients/ClaudeDesktopConfigurator.cs b/MCPForUnity/Editor/Clients/ClaudeDesktopConfigurator.cs index e1678244d..f67e83081 100644 --- a/MCPForUnity/Editor/Clients/ClaudeDesktopConfigurator.cs +++ b/MCPForUnity/Editor/Clients/ClaudeDesktopConfigurator.cs @@ -22,9 +22,9 @@ public ClaudeDesktopConfigurator() : base(new McpClient public override IList GetInstallationSteps() => new List { "Open Claude Desktop", - "Settings > Developer > Edit Config (or open path)", - "Paste JSON", - "Save and restart" + "Go to Settings > Developer > Edit Config\nOR open the config path", + "Paste the configuration JSON", + "Save and restart Claude Desktop" }; public override void Configure() diff --git a/MCPForUnity/Editor/Clients/CodexConfigurator.cs b/MCPForUnity/Editor/Clients/CodexConfigurator.cs index 8cd512d49..30cf5465f 100644 --- a/MCPForUnity/Editor/Clients/CodexConfigurator.cs +++ b/MCPForUnity/Editor/Clients/CodexConfigurator.cs @@ -18,8 +18,8 @@ public CodexConfigurator() : base(new McpClient public override IList GetInstallationSteps() => new List { - "Run 'codex config edit' or open the config path", - "Paste the TOML", + "Run 'codex config edit' in a terminal\nOR open the config file at the path above", + "Paste the configuration TOML", "Save and restart Codex" }; } diff --git a/MCPForUnity/Editor/Clients/CursorConfigurator.cs b/MCPForUnity/Editor/Clients/CursorConfigurator.cs index 42d67acec..dee9736cb 100644 --- a/MCPForUnity/Editor/Clients/CursorConfigurator.cs +++ b/MCPForUnity/Editor/Clients/CursorConfigurator.cs @@ -19,9 +19,9 @@ public CursorConfigurator() : base(new McpClient public override IList GetInstallationSteps() => new List { "Open Cursor", - "Settings > MCP > Add new global MCP server", - "Paste config JSON", - "Save and restart" + "Go to File > Preferences > Cursor Settings > MCP > Add new global MCP server\nOR open the config file at the path above", + "Paste the configuration JSON", + "Save and restart Cursor" }; } } diff --git a/MCPForUnity/Editor/Clients/KiroConfigurator.cs b/MCPForUnity/Editor/Clients/KiroConfigurator.cs index c14d07b28..7d0197520 100644 --- a/MCPForUnity/Editor/Clients/KiroConfigurator.cs +++ b/MCPForUnity/Editor/Clients/KiroConfigurator.cs @@ -21,9 +21,9 @@ public KiroConfigurator() : base(new McpClient public override IList GetInstallationSteps() => new List { "Open Kiro", - "Settings > search \"MCP\" > Open Workspace MCP Config", - "Paste JSON", - "Save and restart" + "Go to File > Settings > Settings > Search for \"MCP\" > Open Workspace MCP Config\nOR open the config file at the path above", + "Paste the configuration JSON", + "Save and restart Kiro" }; } } diff --git a/MCPForUnity/Editor/Clients/TraeConfigurator.cs b/MCPForUnity/Editor/Clients/TraeConfigurator.cs index 2b35142ce..16c87a3d8 100644 --- a/MCPForUnity/Editor/Clients/TraeConfigurator.cs +++ b/MCPForUnity/Editor/Clients/TraeConfigurator.cs @@ -18,9 +18,13 @@ public TraeConfigurator() : base(new McpClient public override IList GetInstallationSteps() => new List { - "Open Trae > Settings > MCP > Add Manually", - "Paste JSON or point to mcp.json", - "Save and restart" + "Open Trae and go to Settings > MCP", + "Select Add Server > Add Manually", + "Paste the JSON or point to the mcp.json file\n"+ + "Windows: %AppData%\\Trae\\mcp.json\n" + + "macOS: ~/Library/Application Support/Trae/mcp.json\n" + + "Linux: ~/.config/Trae/mcp.json\n", + "Save and restart Trae" }; } } diff --git a/MCPForUnity/Editor/Clients/VSCodeConfigurator.cs b/MCPForUnity/Editor/Clients/VSCodeConfigurator.cs index ac3ad9055..e5c9da85d 100644 --- a/MCPForUnity/Editor/Clients/VSCodeConfigurator.cs +++ b/MCPForUnity/Editor/Clients/VSCodeConfigurator.cs @@ -20,8 +20,8 @@ public VSCodeConfigurator() : base(new McpClient public override IList GetInstallationSteps() => new List { "Install GitHub Copilot extension", - "Open/Create mcp.json at the path", - "Paste JSON", + "Open or create mcp.json at the path above", + "Paste the configuration JSON", "Save and restart VSCode" }; } diff --git a/MCPForUnity/Editor/Clients/WindsurfConfigurator.cs b/MCPForUnity/Editor/Clients/WindsurfConfigurator.cs index 83fb94214..6a5bf9d62 100644 --- a/MCPForUnity/Editor/Clients/WindsurfConfigurator.cs +++ b/MCPForUnity/Editor/Clients/WindsurfConfigurator.cs @@ -22,9 +22,9 @@ public WindsurfConfigurator() : base(new McpClient public override IList GetInstallationSteps() => new List { "Open Windsurf", - "Settings > MCP > Manage MCPs > View raw config", - "Paste JSON", - "Save and restart" + "Go to File > Preferences > Windsurf Settings > MCP > Manage MCPs > View raw config\nOR open the config file at the path above", + "Paste the configuration JSON", + "Save and restart Windsurf" }; public override string GetManualSnippet() From 0056d9e6fe5a511a4ef3165f5aa4341764f0a3b5 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Thu, 27 Nov 2025 01:18:38 -0400 Subject: [PATCH 03/14] Move configurators to their own folder It's less clusterd --- MCPForUnity/Editor/Clients/Configurators.meta | 8 ++++++++ .../Clients/{ => Configurators}/ClaudeCodeConfigurator.cs | 2 +- .../{ => Configurators}/ClaudeCodeConfigurator.cs.meta | 0 .../{ => Configurators}/ClaudeDesktopConfigurator.cs | 2 +- .../{ => Configurators}/ClaudeDesktopConfigurator.cs.meta | 0 .../Clients/{ => Configurators}/CodexConfigurator.cs | 2 +- .../Clients/{ => Configurators}/CodexConfigurator.cs.meta | 0 .../Clients/{ => Configurators}/CursorConfigurator.cs | 2 +- .../{ => Configurators}/CursorConfigurator.cs.meta | 0 .../Clients/{ => Configurators}/KiroConfigurator.cs | 2 +- .../Clients/{ => Configurators}/KiroConfigurator.cs.meta | 0 .../Clients/{ => Configurators}/TraeConfigurator.cs | 2 +- .../Clients/{ => Configurators}/TraeConfigurator.cs.meta | 0 .../Clients/{ => Configurators}/VSCodeConfigurator.cs | 2 +- .../{ => Configurators}/VSCodeConfigurator.cs.meta | 0 .../Clients/{ => Configurators}/WindsurfConfigurator.cs | 2 +- .../{ => Configurators}/WindsurfConfigurator.cs.meta | 0 17 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 MCPForUnity/Editor/Clients/Configurators.meta rename MCPForUnity/Editor/Clients/{ => Configurators}/ClaudeCodeConfigurator.cs (93%) rename MCPForUnity/Editor/Clients/{ => Configurators}/ClaudeCodeConfigurator.cs.meta (100%) rename MCPForUnity/Editor/Clients/{ => Configurators}/ClaudeDesktopConfigurator.cs (97%) rename MCPForUnity/Editor/Clients/{ => Configurators}/ClaudeDesktopConfigurator.cs.meta (100%) rename MCPForUnity/Editor/Clients/{ => Configurators}/CodexConfigurator.cs (95%) rename MCPForUnity/Editor/Clients/{ => Configurators}/CodexConfigurator.cs.meta (100%) rename MCPForUnity/Editor/Clients/{ => Configurators}/CursorConfigurator.cs (95%) rename MCPForUnity/Editor/Clients/{ => Configurators}/CursorConfigurator.cs.meta (100%) rename MCPForUnity/Editor/Clients/{ => Configurators}/KiroConfigurator.cs (95%) rename MCPForUnity/Editor/Clients/{ => Configurators}/KiroConfigurator.cs.meta (100%) rename MCPForUnity/Editor/Clients/{ => Configurators}/TraeConfigurator.cs (95%) rename MCPForUnity/Editor/Clients/{ => Configurators}/TraeConfigurator.cs.meta (100%) rename MCPForUnity/Editor/Clients/{ => Configurators}/VSCodeConfigurator.cs (95%) rename MCPForUnity/Editor/Clients/{ => Configurators}/VSCodeConfigurator.cs.meta (100%) rename MCPForUnity/Editor/Clients/{ => Configurators}/WindsurfConfigurator.cs (96%) rename MCPForUnity/Editor/Clients/{ => Configurators}/WindsurfConfigurator.cs.meta (100%) diff --git a/MCPForUnity/Editor/Clients/Configurators.meta b/MCPForUnity/Editor/Clients/Configurators.meta new file mode 100644 index 000000000..a259c217d --- /dev/null +++ b/MCPForUnity/Editor/Clients/Configurators.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 59ff83375c2c74c8385c4a22549778dd +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Clients/ClaudeCodeConfigurator.cs b/MCPForUnity/Editor/Clients/Configurators/ClaudeCodeConfigurator.cs similarity index 93% rename from MCPForUnity/Editor/Clients/ClaudeCodeConfigurator.cs rename to MCPForUnity/Editor/Clients/Configurators/ClaudeCodeConfigurator.cs index 056e1ac9a..1c8bf734d 100644 --- a/MCPForUnity/Editor/Clients/ClaudeCodeConfigurator.cs +++ b/MCPForUnity/Editor/Clients/Configurators/ClaudeCodeConfigurator.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using MCPForUnity.Editor.Models; -namespace MCPForUnity.Editor.Clients +namespace MCPForUnity.Editor.Clients.Configurators { public class ClaudeCodeConfigurator : ClaudeCliMcpConfigurator { diff --git a/MCPForUnity/Editor/Clients/ClaudeCodeConfigurator.cs.meta b/MCPForUnity/Editor/Clients/Configurators/ClaudeCodeConfigurator.cs.meta similarity index 100% rename from MCPForUnity/Editor/Clients/ClaudeCodeConfigurator.cs.meta rename to MCPForUnity/Editor/Clients/Configurators/ClaudeCodeConfigurator.cs.meta diff --git a/MCPForUnity/Editor/Clients/ClaudeDesktopConfigurator.cs b/MCPForUnity/Editor/Clients/Configurators/ClaudeDesktopConfigurator.cs similarity index 97% rename from MCPForUnity/Editor/Clients/ClaudeDesktopConfigurator.cs rename to MCPForUnity/Editor/Clients/Configurators/ClaudeDesktopConfigurator.cs index f67e83081..e6cb41b17 100644 --- a/MCPForUnity/Editor/Clients/ClaudeDesktopConfigurator.cs +++ b/MCPForUnity/Editor/Clients/Configurators/ClaudeDesktopConfigurator.cs @@ -5,7 +5,7 @@ using MCPForUnity.Editor.Models; using UnityEditor; -namespace MCPForUnity.Editor.Clients +namespace MCPForUnity.Editor.Clients.Configurators { public class ClaudeDesktopConfigurator : JsonFileMcpConfigurator { diff --git a/MCPForUnity/Editor/Clients/ClaudeDesktopConfigurator.cs.meta b/MCPForUnity/Editor/Clients/Configurators/ClaudeDesktopConfigurator.cs.meta similarity index 100% rename from MCPForUnity/Editor/Clients/ClaudeDesktopConfigurator.cs.meta rename to MCPForUnity/Editor/Clients/Configurators/ClaudeDesktopConfigurator.cs.meta diff --git a/MCPForUnity/Editor/Clients/CodexConfigurator.cs b/MCPForUnity/Editor/Clients/Configurators/CodexConfigurator.cs similarity index 95% rename from MCPForUnity/Editor/Clients/CodexConfigurator.cs rename to MCPForUnity/Editor/Clients/Configurators/CodexConfigurator.cs index 30cf5465f..9337d4cd5 100644 --- a/MCPForUnity/Editor/Clients/CodexConfigurator.cs +++ b/MCPForUnity/Editor/Clients/Configurators/CodexConfigurator.cs @@ -3,7 +3,7 @@ using System.IO; using MCPForUnity.Editor.Models; -namespace MCPForUnity.Editor.Clients +namespace MCPForUnity.Editor.Clients.Configurators { public class CodexConfigurator : CodexMcpConfigurator { diff --git a/MCPForUnity/Editor/Clients/CodexConfigurator.cs.meta b/MCPForUnity/Editor/Clients/Configurators/CodexConfigurator.cs.meta similarity index 100% rename from MCPForUnity/Editor/Clients/CodexConfigurator.cs.meta rename to MCPForUnity/Editor/Clients/Configurators/CodexConfigurator.cs.meta diff --git a/MCPForUnity/Editor/Clients/CursorConfigurator.cs b/MCPForUnity/Editor/Clients/Configurators/CursorConfigurator.cs similarity index 95% rename from MCPForUnity/Editor/Clients/CursorConfigurator.cs rename to MCPForUnity/Editor/Clients/Configurators/CursorConfigurator.cs index dee9736cb..d63b22610 100644 --- a/MCPForUnity/Editor/Clients/CursorConfigurator.cs +++ b/MCPForUnity/Editor/Clients/Configurators/CursorConfigurator.cs @@ -3,7 +3,7 @@ using System.IO; using MCPForUnity.Editor.Models; -namespace MCPForUnity.Editor.Clients +namespace MCPForUnity.Editor.Clients.Configurators { public class CursorConfigurator : JsonFileMcpConfigurator { diff --git a/MCPForUnity/Editor/Clients/CursorConfigurator.cs.meta b/MCPForUnity/Editor/Clients/Configurators/CursorConfigurator.cs.meta similarity index 100% rename from MCPForUnity/Editor/Clients/CursorConfigurator.cs.meta rename to MCPForUnity/Editor/Clients/Configurators/CursorConfigurator.cs.meta diff --git a/MCPForUnity/Editor/Clients/KiroConfigurator.cs b/MCPForUnity/Editor/Clients/Configurators/KiroConfigurator.cs similarity index 95% rename from MCPForUnity/Editor/Clients/KiroConfigurator.cs rename to MCPForUnity/Editor/Clients/Configurators/KiroConfigurator.cs index 7d0197520..445b6e597 100644 --- a/MCPForUnity/Editor/Clients/KiroConfigurator.cs +++ b/MCPForUnity/Editor/Clients/Configurators/KiroConfigurator.cs @@ -3,7 +3,7 @@ using System.IO; using MCPForUnity.Editor.Models; -namespace MCPForUnity.Editor.Clients +namespace MCPForUnity.Editor.Clients.Configurators { public class KiroConfigurator : JsonFileMcpConfigurator { diff --git a/MCPForUnity/Editor/Clients/KiroConfigurator.cs.meta b/MCPForUnity/Editor/Clients/Configurators/KiroConfigurator.cs.meta similarity index 100% rename from MCPForUnity/Editor/Clients/KiroConfigurator.cs.meta rename to MCPForUnity/Editor/Clients/Configurators/KiroConfigurator.cs.meta diff --git a/MCPForUnity/Editor/Clients/TraeConfigurator.cs b/MCPForUnity/Editor/Clients/Configurators/TraeConfigurator.cs similarity index 95% rename from MCPForUnity/Editor/Clients/TraeConfigurator.cs rename to MCPForUnity/Editor/Clients/Configurators/TraeConfigurator.cs index 16c87a3d8..f32b68863 100644 --- a/MCPForUnity/Editor/Clients/TraeConfigurator.cs +++ b/MCPForUnity/Editor/Clients/Configurators/TraeConfigurator.cs @@ -3,7 +3,7 @@ using System.IO; using MCPForUnity.Editor.Models; -namespace MCPForUnity.Editor.Clients +namespace MCPForUnity.Editor.Clients.Configurators { public class TraeConfigurator : JsonFileMcpConfigurator { diff --git a/MCPForUnity/Editor/Clients/TraeConfigurator.cs.meta b/MCPForUnity/Editor/Clients/Configurators/TraeConfigurator.cs.meta similarity index 100% rename from MCPForUnity/Editor/Clients/TraeConfigurator.cs.meta rename to MCPForUnity/Editor/Clients/Configurators/TraeConfigurator.cs.meta diff --git a/MCPForUnity/Editor/Clients/VSCodeConfigurator.cs b/MCPForUnity/Editor/Clients/Configurators/VSCodeConfigurator.cs similarity index 95% rename from MCPForUnity/Editor/Clients/VSCodeConfigurator.cs rename to MCPForUnity/Editor/Clients/Configurators/VSCodeConfigurator.cs index e5c9da85d..90579304d 100644 --- a/MCPForUnity/Editor/Clients/VSCodeConfigurator.cs +++ b/MCPForUnity/Editor/Clients/Configurators/VSCodeConfigurator.cs @@ -3,7 +3,7 @@ using System.IO; using MCPForUnity.Editor.Models; -namespace MCPForUnity.Editor.Clients +namespace MCPForUnity.Editor.Clients.Configurators { public class VSCodeConfigurator : JsonFileMcpConfigurator { diff --git a/MCPForUnity/Editor/Clients/VSCodeConfigurator.cs.meta b/MCPForUnity/Editor/Clients/Configurators/VSCodeConfigurator.cs.meta similarity index 100% rename from MCPForUnity/Editor/Clients/VSCodeConfigurator.cs.meta rename to MCPForUnity/Editor/Clients/Configurators/VSCodeConfigurator.cs.meta diff --git a/MCPForUnity/Editor/Clients/WindsurfConfigurator.cs b/MCPForUnity/Editor/Clients/Configurators/WindsurfConfigurator.cs similarity index 96% rename from MCPForUnity/Editor/Clients/WindsurfConfigurator.cs rename to MCPForUnity/Editor/Clients/Configurators/WindsurfConfigurator.cs index 6a5bf9d62..0e15bff2a 100644 --- a/MCPForUnity/Editor/Clients/WindsurfConfigurator.cs +++ b/MCPForUnity/Editor/Clients/Configurators/WindsurfConfigurator.cs @@ -3,7 +3,7 @@ using System.IO; using MCPForUnity.Editor.Models; -namespace MCPForUnity.Editor.Clients +namespace MCPForUnity.Editor.Clients.Configurators { public class WindsurfConfigurator : JsonFileMcpConfigurator { diff --git a/MCPForUnity/Editor/Clients/WindsurfConfigurator.cs.meta b/MCPForUnity/Editor/Clients/Configurators/WindsurfConfigurator.cs.meta similarity index 100% rename from MCPForUnity/Editor/Clients/WindsurfConfigurator.cs.meta rename to MCPForUnity/Editor/Clients/Configurators/WindsurfConfigurator.cs.meta From afcd2ef469c5d9c4e87ec132d5f782dd9c87be67 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Thu, 27 Nov 2025 13:23:10 -0400 Subject: [PATCH 04/14] Remvoe override for Windsurf because we no longer need to use it --- .../Editor/Clients/Configurators/WindsurfConfigurator.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/MCPForUnity/Editor/Clients/Configurators/WindsurfConfigurator.cs b/MCPForUnity/Editor/Clients/Configurators/WindsurfConfigurator.cs index 0e15bff2a..4437170fa 100644 --- a/MCPForUnity/Editor/Clients/Configurators/WindsurfConfigurator.cs +++ b/MCPForUnity/Editor/Clients/Configurators/WindsurfConfigurator.cs @@ -26,11 +26,5 @@ public WindsurfConfigurator() : base(new McpClient "Paste the configuration JSON", "Save and restart Windsurf" }; - - public override string GetManualSnippet() - { - // Force consistent handling for Windsurf; reuse base behavior - return base.GetManualSnippet(); - } } } From 5e02e9dfac130da27e848e2a0cab787f792a60f5 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Thu, 27 Nov 2025 15:04:55 -0400 Subject: [PATCH 05/14] Add Antigravity configs Works like Windsurf, but it sucks ass --- .../Configurators/AntigravityConfigurator.cs | 32 +++++++++++++++++++ .../AntigravityConfigurator.cs.meta | 11 +++++++ 2 files changed, 43 insertions(+) create mode 100644 MCPForUnity/Editor/Clients/Configurators/AntigravityConfigurator.cs create mode 100644 MCPForUnity/Editor/Clients/Configurators/AntigravityConfigurator.cs.meta diff --git a/MCPForUnity/Editor/Clients/Configurators/AntigravityConfigurator.cs b/MCPForUnity/Editor/Clients/Configurators/AntigravityConfigurator.cs new file mode 100644 index 000000000..9a83620f6 --- /dev/null +++ b/MCPForUnity/Editor/Clients/Configurators/AntigravityConfigurator.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.IO; +using MCPForUnity.Editor.Constants; +using MCPForUnity.Editor.Models; +using UnityEditor; + +namespace MCPForUnity.Editor.Clients.Configurators +{ + public class AntigravityConfigurator : JsonFileMcpConfigurator + { + public AntigravityConfigurator() : base(new McpClient + { + name = "Antigravity", + windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "antigravity", "mcp_config.json"), + macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "antigravity", "mcp_config.json"), + linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "antigravity", "mcp_config.json"), + HttpUrlProperty = "serverUrl", + DefaultUnityFields = { { "disabled", false } }, + StripEnvWhenNotRequired = true + }) + { } + + public override IList GetInstallationSteps() => new List + { + "Open Antigravity", + "Click the more_horiz menu in the Agent pane > MCP Servers", + "Select 'Install' for Unity MCP or use the Configure button above", + "Restart Antigravity if necessary" + }; + } +} diff --git a/MCPForUnity/Editor/Clients/Configurators/AntigravityConfigurator.cs.meta b/MCPForUnity/Editor/Clients/Configurators/AntigravityConfigurator.cs.meta new file mode 100644 index 000000000..76e91a072 --- /dev/null +++ b/MCPForUnity/Editor/Clients/Configurators/AntigravityConfigurator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 331b33961513042e3945d0a1d06615b5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From dbdbbb8dce8efca8688726622655fe6c6677fc91 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Thu, 27 Nov 2025 15:05:02 -0400 Subject: [PATCH 06/14] Add some docs for properties --- MCPForUnity/Editor/Models/McpClient.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/MCPForUnity/Editor/Models/McpClient.cs b/MCPForUnity/Editor/Models/McpClient.cs index 480c021e0..f09352a0d 100644 --- a/MCPForUnity/Editor/Models/McpClient.cs +++ b/MCPForUnity/Editor/Models/McpClient.cs @@ -12,11 +12,11 @@ public class McpClient public McpStatus status = McpStatus.NotConfigured; // Capability flags/config for JSON-based configurators - public bool IsVsCodeLayout; - public bool SupportsHttpTransport = true; - public bool EnsureEnvObject; - public bool StripEnvWhenNotRequired; - public string HttpUrlProperty = "url"; + public bool IsVsCodeLayout; // Whether the config file follows VS Code layout (env object at root) + public bool SupportsHttpTransport = true; // Whether the MCP server supports HTTP transport + public bool EnsureEnvObject; // Whether to ensure the env object is present in the config + public bool StripEnvWhenNotRequired; // Whether to strip the env object when not required + public string HttpUrlProperty = "url"; // The property name for the HTTP URL in the config public Dictionary DefaultUnityFields = new(); // Helper method to convert the enum to a display string From 7ff69363b170f937e2dfe64ffd1ce7f299dc441f Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Thu, 27 Nov 2025 15:38:30 -0400 Subject: [PATCH 07/14] Add comprehensive MCP client configurators documentation --- docs/MCP_CLIENT_CONFIGURATORS.md | 290 +++++++++++++++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 docs/MCP_CLIENT_CONFIGURATORS.md diff --git a/docs/MCP_CLIENT_CONFIGURATORS.md b/docs/MCP_CLIENT_CONFIGURATORS.md new file mode 100644 index 000000000..097aee62e --- /dev/null +++ b/docs/MCP_CLIENT_CONFIGURATORS.md @@ -0,0 +1,290 @@ +# MCP Client Configurators + +This guide explains how MCP client configurators work in this repo and how to add a new one. + +It covers: + +- **Typical JSON-file clients** (Cursor, VSCode GitHub Copilot, Windsurf, Kiro, Trae, Antigravity, etc.). +- **Special clients** like **Claude CLI** and **Codex** that require custom logic. +- **How to add a new configurator class** so it shows up automatically in the MCP for Unity window. + +## Quick example: JSON-file configurator + +For most clients you just need a small class like this: + +```csharp +using System; +using System.Collections.Generic; +using System.IO; +using MCPForUnity.Editor.Models; + +namespace MCPForUnity.Editor.Clients.Configurators +{ + public class MyClientConfigurator : JsonFileMcpConfigurator + { + public MyClientConfigurator() : base(new McpClient + { + name = "My Client", + windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".myclient", "mcp.json"), + macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".myclient", "mcp.json"), + linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".myclient", "mcp.json"), + }) + { } + + public override IList GetInstallationSteps() => new List + { + "Open My Client and go to MCP settings", + "Open or create the mcp.json file at the path above", + "Click Configure in MCP for Unity (or paste the manual JSON snippet)", + "Restart My Client" + }; + } +} +``` + +--- + +## How the configurator system works + +At a high level: + +- **`IMcpClientConfigurator`** (`MCPForUnity/Editor/Clients/IMcpClientConfigurator.cs`) + - Contract for all MCP client configurators. + - Handles status detection, auto-configure, manual snippet, and installation steps. + +- **Base classes** (`MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs`) + - **`McpClientConfiguratorBase`** + - Common properties and helpers. + - **`JsonFileMcpConfigurator`** + - For JSON-based config files (most clients). + - Implements `CheckStatus`, `Configure`, and `GetManualSnippet` using `ConfigJsonBuilder`. + - **`CodexMcpConfigurator`** + - For Codex-style TOML config files. + - **`ClaudeCliMcpConfigurator`** + - For CLI-driven clients like Claude Code (register/unregister via CLI, not JSON files). + +- **`McpClient` model** (`MCPForUnity/Editor/Models/McpClient.cs`) + - Holds the per-client configuration: + - `name` + - `windowsConfigPath`, `macConfigPath`, `linuxConfigPath` + - Status and several **JSON-config flags** (used by `JsonFileMcpConfigurator`): + - `IsVsCodeLayout` – VS Code-style layout (`servers` root, `type` field, etc.). + - `SupportsHttpTransport` – whether the client supports HTTP transport. + - `EnsureEnvObject` – ensure an `env` object exists. + - `StripEnvWhenNotRequired` – remove `env` when not needed. + - `HttpUrlProperty` – which property holds the HTTP URL (e.g. `"url"` vs `"serverUrl"`). + - `DefaultUnityFields` – key/value pairs like `{ "disabled": false }` applied when missing. + +- **Auto-discovery** (`McpClientRegistry`) + - `McpClientRegistry.All` uses `TypeCache.GetTypesDerivedFrom()` to find configurators. + - A configurator appears automatically if: + - It is a **public, non-abstract class**. + - It has a **public parameterless constructor**. + - No extra registration list is required. + +--- + +## Typical JSON-file clients + +Most MCP clients use a JSON config file that defines one or more MCP servers. Examples: + +- **Cursor** – `JsonFileMcpConfigurator` (global `~/.cursor/mcp.json`). +- **VSCode GitHub Copilot** – `JsonFileMcpConfigurator` with `IsVsCodeLayout = true`. +- **Windsurf** – `JsonFileMcpConfigurator` with Windsurf-specific flags (`HttpUrlProperty = "serverUrl"`, `DefaultUnityFields["disabled"] = false`, etc.). +- **Kiro**, **Trae**, **Antigravity (Gemini)** – JSON configs with project-specific paths and flags. + +All of these follow the same pattern: + +1. **Subclass `JsonFileMcpConfigurator`.** +2. **Provide a `McpClient` instance** in the constructor with: + - A user-friendly `name`. + - OS-specific config paths. + - Any JSON behavior flags as needed. +3. **Override `GetInstallationSteps`** to describe how users open or edit the config. +4. Rely on **base implementations** for: + - `CheckStatus` – reads and validates the JSON config; can auto-rewrite to match Unity MCP. + - `Configure` – writes/rewrites the config file. + - `GetManualSnippet` – builds a JSON snippet using `ConfigJsonBuilder`. + +### JSON behavior controlled by `McpClient` + +`JsonFileMcpConfigurator` relies on the fields on `McpClient`: + +- **HTTP vs stdio** + - `SupportsHttpTransport` + `EditorPrefs.UseHttpTransport` decide whether to configure + - `url` / `serverUrl` (HTTP), or + - `command` + `args` (stdio with `uvx`). +- **URL property name** + - `HttpUrlProperty` (default `"url"`) selects which JSON property to use for HTTP urls. + - Example: Windsurf and Antigravity use `"serverUrl"`. +- **VS Code layout** + - `IsVsCodeLayout = true` switches config structure to a VS Code compatible layout. +- **Env object and default fields** + - `EnsureEnvObject` / `StripEnvWhenNotRequired` control an `env` block. + - `DefaultUnityFields` adds client-specific fields if they are missing (e.g. `disabled: false`). + +All of this logic is centralized in **`ConfigJsonBuilder`**, so most JSON-based clients **do not need to override** `GetManualSnippet`. + +--- + +## Special clients + +Some clients cannot be handled by the generic JSON configurator alone. + +### Codex (TOML-based) + +- Uses **`CodexMcpConfigurator`**. +- Reads and writes a **TOML** config (usually `~/.codex/config.toml`). +- Uses `CodexConfigHelper` to: + - Parse the existing TOML. + - Check for a matching Unity MCP server configuration. + - Write/patch the Codex server block. +- The `CodexConfigurator` class: + - Only needs to supply a `McpClient` with TOML config paths. + - Inherits the Codex-specific status and configure behavior from `CodexMcpConfigurator`. + +### Claude Code (CLI-based) + +- Uses **`ClaudeCliMcpConfigurator`**. +- Configuration is stored **internally by the Claude CLI**, not in a JSON file. +- `CheckStatus` and `Configure` are implemented in the base class using `claude mcp ...` commands: + - `CheckStatus` calls `claude mcp list` to detect if `UnityMCP` is registered. + - `Configure` toggles register/unregister via `claude mcp add/remove UnityMCP`. +- The `ClaudeCodeConfigurator` class: + - Only needs a `McpClient` with a `name`. + - Overrides `GetInstallationSteps` with CLI-specific instructions. + +### Claude Desktop (JSON with restrictions) + +- Uses **`JsonFileMcpConfigurator`**, but only supports **stdio transport**. +- `ClaudeDesktopConfigurator`: + - Sets `SupportsHttpTransport = false` in `McpClient`. + - Overrides `Configure` / `GetManualSnippet` to: + - Guard against HTTP mode. + - Provide clear error text if HTTP is enabled. + +--- + +## Adding a new MCP client (typical JSON case) + +This is the most common scenario: your MCP client uses a JSON file to configure servers. + +### 1. Choose the base class + +- Use **`JsonFileMcpConfigurator`** if your client reads a JSON config file. +- Consider **`CodexMcpConfigurator`** only if you are integrating a TOML-based client like Codex. +- Consider **`ClaudeCliMcpConfigurator`** only if your client exposes a CLI command to manage MCP servers. + +### 2. Create the configurator class + +Create a new file under: + +```text +MCPForUnity/Editor/Clients/Configurators +``` + +Name it something like: + +```text +MyClientConfigurator.cs +``` + +Inside, follow the existing pattern (e.g. `CursorConfigurator`, `WindsurfConfigurator`, `KiroConfigurator`): + +- **Namespace** must be: + - `MCPForUnity.Editor.Clients.Configurators` +- **Class**: + - `public class MyClientConfigurator : JsonFileMcpConfigurator` +- **Constructor**: + - Public, **parameterless**, and call `base(new McpClient { ... })`. + - Set at least: + - `name = "My Client"` + - `windowsConfigPath = ...` + - `macConfigPath = ...` + - `linuxConfigPath = ...` + - Optionally set flags: + - `IsVsCodeLayout = true` for VS Code-style config. + - `HttpUrlProperty = "serverUrl"` if your client expects `serverUrl`. + - `EnsureEnvObject` / `StripEnvWhenNotRequired` based on env handling. + - `DefaultUnityFields = { { "disabled", false }, ... }` for client-specific defaults. + +Because the constructor is parameterless and public, **`McpClientRegistry` will auto-discover this configurator** with no extra registration. + +### 3. Add installation steps + +Override `GetInstallationSteps` to tell users how to configure the client: + +- Where to find or create the JSON config file. +- Which menu path opens the MCP settings. +- Whether they should rely on the **Configure** button or copy-paste the manual JSON. + +Look at `CursorConfigurator`, `VSCodeConfigurator`, `KiroConfigurator`, `TraeConfigurator`, or `AntigravityConfigurator` for phrasing. + +### 4. Rely on the base JSON logic + +Unless your client has very unusual behavior, you typically **do not need to override**: + +- `CheckStatus` +- `Configure` +- `GetManualSnippet` + +The base `JsonFileMcpConfigurator`: + +- Detects missing or mismatched config. +- Optionally rewrites config to match Unity MCP. +- Builds a JSON snippet with **correct HTTP vs stdio settings**, using `ConfigJsonBuilder`. + +Only override these methods if your client has constraints that cannot be expressed via `McpClient` flags. + +### 5. Verify in Unity + +After adding your configurator class: + +1. Open Unity and the **MCP for Unity** window. +2. Your client should appear in the list, sorted by display name (`McpClient.name`). +3. Use **Check Status** to verify: + - Missing config files show as `Not Configured`. + - Existing files with matching server settings show as `Configured`. +4. Click **Configure** to auto-write the config file. +5. Restart your MCP client and confirm it connects to Unity. + +--- + +## Adding a custom (non-JSON) client + +If your MCP client doesnt store configuration as a JSON file, you likely need a custom base class. + +### Codex-style TOML client + +- Subclass **`CodexMcpConfigurator`**. +- Provide TOML paths via `McpClient` (similar to `CodexConfigurator`). +- Override `GetInstallationSteps` to describe how to open/edit the TOML. + +The Codex-specific status and configure logic is already implemented in the base class. + +### CLI-managed client (Claude-style) + +- Subclass **`ClaudeCliMcpConfigurator`**. +- Provide a `McpClient` with a `name`. +- Override `GetInstallationSteps` with the CLI flow. + +The base class: + +- Locates the CLI binary using `MCPServiceLocator.Paths`. +- Uses `ExecPath.TryRun` to call `mcp list`, `mcp add`, and `mcp remove`. +- Implements `Configure` as a toggle between register and unregister. + +Use this only if the client exposes an official CLI for managing MCP servers. + +--- + +## Summary + +- **For most MCP clients**, you only need to: + - Create a `JsonFileMcpConfigurator` subclass in `Editor/Clients/Configurators`. + - Provide a `McpClient` with paths and flags. + - Override `GetInstallationSteps`. +- **Special cases** like Codex (TOML) and Claude Code (CLI) have dedicated base classes. +- **No manual registration** is needed: `McpClientRegistry` auto-discovers all configurators with a public parameterless constructor. + +Following these patterns keeps all MCP client integrations consistent and lets users configure everything from the MCP for Unity window with minimal friction. From 24fd68e068a735d8ff7d6e700e3de355baa100a9 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Thu, 27 Nov 2025 17:07:25 -0400 Subject: [PATCH 08/14] Add missing imports (#7) --- Server/src/transport/legacy/port_discovery.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Server/src/transport/legacy/port_discovery.py b/Server/src/transport/legacy/port_discovery.py index fe7de5bf6..2143106f8 100644 --- a/Server/src/transport/legacy/port_discovery.py +++ b/Server/src/transport/legacy/port_discovery.py @@ -14,9 +14,11 @@ import glob import json import logging +import os from datetime import datetime from pathlib import Path import socket +import struct from models.models import UnityInstanceInfo From 684aeb9e6c4dad3a6607213d5a550aecfd62b61a Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Thu, 27 Nov 2025 17:16:43 -0400 Subject: [PATCH 09/14] Handle Linux paths when unregistering CLI commands --- .../Editor/Clients/McpClientConfiguratorBase.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs index 503b2ef91..0a1a331ce 100644 --- a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs +++ b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs @@ -482,9 +482,15 @@ private void Unregister() } string projectDir = Path.GetDirectoryName(Application.dataPath); - string pathPrepend = Application.platform == RuntimePlatform.OSXEditor - ? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin" - : null; + string pathPrepend = null; + if (Application.platform == RuntimePlatform.OSXEditor) + { + pathPrepend = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"; + } + else if (Application.platform == RuntimePlatform.LinuxEditor) + { + pathPrepend = "/usr/local/bin:/usr/bin:/bin"; + } bool serverExists = ExecPath.TryRun(claudePath, "mcp get UnityMCP", projectDir, out _, out _, 7000, pathPrepend); From 63496c5694468286bdc39f53cb10c1effcb1c0d9 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Thu, 27 Nov 2025 17:17:11 -0400 Subject: [PATCH 10/14] Construct a JSON error in a much more secure fashion --- MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs index 0a1a331ce..f1dc2eb98 100644 --- a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs +++ b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs @@ -214,7 +214,8 @@ public override string GetManualSnippet() } catch (Exception ex) { - return $"{{ \"error\": \"{ex.Message}\" }}"; + var errorObj = new { error = ex.Message }; + return JsonConvert.SerializeObject(errorObj); } } From 01f5aeb23957b3d6736d3c4f7cf6540b6ba06c91 Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Thu, 27 Nov 2025 18:18:09 -0400 Subject: [PATCH 11/14] Fix stdio auto-reconnect after domain reloads We mirror what we've done with the HTTP/websocket connection We also ensure the states from the stdio/HTTP connections are handled separately. Things now work as expected --- .../Editor/Constants/EditorPrefKeys.cs | 1 + .../Editor/Services/BridgeControlService.cs | 29 +++-- .../Services/HttpBridgeReloadHandler.cs | 16 +-- .../Services/StdioBridgeReloadHandler.cs | 90 +++++++++++++++ .../Services/StdioBridgeReloadHandler.cs.meta | 11 ++ .../Services/Transport/TransportManager.cs | 106 +++++++++++------- .../Transport/Transports/StdioBridgeHost.cs | 50 --------- 7 files changed, 195 insertions(+), 108 deletions(-) create mode 100644 MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs create mode 100644 MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs.meta diff --git a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs index ffaa31c00..30dcd2bb3 100644 --- a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs +++ b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs @@ -11,6 +11,7 @@ internal static class EditorPrefKeys internal const string ValidationLevel = "MCPForUnity.ValidationLevel"; internal const string UnitySocketPort = "MCPForUnity.UnitySocketPort"; internal const string ResumeHttpAfterReload = "MCPForUnity.ResumeHttpAfterReload"; + internal const string ResumeStdioAfterReload = "MCPForUnity.ResumeStdioAfterReload"; internal const string UvxPathOverride = "MCPForUnity.UvxPath"; internal const string ClaudeCliPathOverride = "MCPForUnity.ClaudeCliPath"; diff --git a/MCPForUnity/Editor/Services/BridgeControlService.cs b/MCPForUnity/Editor/Services/BridgeControlService.cs index c67efd1b6..3e18b67cf 100644 --- a/MCPForUnity/Editor/Services/BridgeControlService.cs +++ b/MCPForUnity/Editor/Services/BridgeControlService.cs @@ -49,13 +49,21 @@ private static BridgeVerificationResult BuildVerificationResult(TransportState s }; } - public bool IsRunning => _transportManager.GetState().IsConnected; + public bool IsRunning + { + get + { + var mode = ResolvePreferredMode(); + return _transportManager.IsRunning(mode); + } + } public int CurrentPort { get { - var state = _transportManager.GetState(); + var mode = ResolvePreferredMode(); + var state = _transportManager.GetState(mode); if (state.Port.HasValue) { return state.Port.Value; @@ -67,7 +75,7 @@ public int CurrentPort } public bool IsAutoConnectMode => StdioBridgeHost.IsAutoConnectMode(); - public TransportMode? ActiveMode => _transportManager.ActiveMode; + public TransportMode? ActiveMode => _preferredMode; public async Task StartAsync() { @@ -92,7 +100,8 @@ public async Task StopAsync() { try { - await _transportManager.StopAsync(); + var mode = ResolvePreferredMode(); + await _transportManager.StopAsync(mode); } catch (Exception ex) { @@ -102,17 +111,17 @@ public async Task StopAsync() public async Task VerifyAsync() { - var mode = _transportManager.ActiveMode ?? ResolvePreferredMode(); - bool pingSucceeded = await _transportManager.VerifyAsync(); - var state = _transportManager.GetState(); + var mode = ResolvePreferredMode(); + bool pingSucceeded = await _transportManager.VerifyAsync(mode); + var state = _transportManager.GetState(mode); return BuildVerificationResult(state, mode, pingSucceeded); } public BridgeVerificationResult Verify(int port) { - var mode = _transportManager.ActiveMode ?? ResolvePreferredMode(); - bool pingSucceeded = _transportManager.VerifyAsync().GetAwaiter().GetResult(); - var state = _transportManager.GetState(); + var mode = ResolvePreferredMode(); + bool pingSucceeded = _transportManager.VerifyAsync(mode).GetAwaiter().GetResult(); + var state = _transportManager.GetState(mode); if (mode == TransportMode.Stdio) { diff --git a/MCPForUnity/Editor/Services/HttpBridgeReloadHandler.cs b/MCPForUnity/Editor/Services/HttpBridgeReloadHandler.cs index 16b8bd87e..0422a92e9 100644 --- a/MCPForUnity/Editor/Services/HttpBridgeReloadHandler.cs +++ b/MCPForUnity/Editor/Services/HttpBridgeReloadHandler.cs @@ -24,8 +24,8 @@ private static void OnBeforeAssemblyReload() { try { - var bridge = MCPServiceLocator.Bridge; - bool shouldResume = bridge.IsRunning && bridge.ActiveMode == TransportMode.Http; + var transport = MCPServiceLocator.TransportManager; + bool shouldResume = transport.IsRunning(TransportMode.Http); if (shouldResume) { @@ -36,9 +36,9 @@ private static void OnBeforeAssemblyReload() EditorPrefs.DeleteKey(EditorPrefKeys.ResumeHttpAfterReload); } - if (bridge.IsRunning) + if (shouldResume) { - var stopTask = bridge.StopAsync(); + var stopTask = transport.StopAsync(TransportMode.Http); stopTask.ContinueWith(t => { if (t.IsFaulted && t.Exception != null) @@ -59,7 +59,9 @@ private static void OnAfterAssemblyReload() bool resume = false; try { - resume = EditorPrefs.GetBool(EditorPrefKeys.ResumeHttpAfterReload, false); + // Only resume HTTP if it is still the selected transport. + bool useHttp = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true); + resume = useHttp && EditorPrefs.GetBool(EditorPrefKeys.ResumeHttpAfterReload, false); if (resume) { EditorPrefs.DeleteKey(EditorPrefKeys.ResumeHttpAfterReload); @@ -90,7 +92,7 @@ private static void OnAfterAssemblyReload() { try { - var startTask = MCPServiceLocator.Bridge.StartAsync(); + var startTask = MCPServiceLocator.TransportManager.StartAsync(TransportMode.Http); startTask.ContinueWith(t => { if (t.IsFaulted) @@ -123,7 +125,7 @@ private static void OnAfterAssemblyReload() { try { - bool started = await MCPServiceLocator.Bridge.StartAsync(); + bool started = await MCPServiceLocator.TransportManager.StartAsync(TransportMode.Http); if (!started) { McpLog.Warn("Failed to resume HTTP MCP bridge after domain reload"); diff --git a/MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs b/MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs new file mode 100644 index 000000000..98921c431 --- /dev/null +++ b/MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs @@ -0,0 +1,90 @@ +using System; +using UnityEditor; +using MCPForUnity.Editor.Constants; +using MCPForUnity.Editor.Helpers; +using MCPForUnity.Editor.Services.Transport; +using MCPForUnity.Editor.Services.Transport.Transports; + +namespace MCPForUnity.Editor.Services +{ + /// + /// Ensures the legacy stdio bridge resumes after domain reloads, mirroring the HTTP handler. + /// + [InitializeOnLoad] + internal static class StdioBridgeReloadHandler + { + static StdioBridgeReloadHandler() + { + AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload; + AssemblyReloadEvents.afterAssemblyReload += OnAfterAssemblyReload; + } + + private static void OnBeforeAssemblyReload() + { + try + { + // Only persist resume intent when stdio is the active transport and the bridge is running. + bool useHttp = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true); + bool isRunning = MCPServiceLocator.TransportManager.IsRunning(TransportMode.Stdio); + if (!useHttp && isRunning) + { + EditorPrefs.SetBool(EditorPrefKeys.ResumeStdioAfterReload, true); + } + else + { + EditorPrefs.DeleteKey(EditorPrefKeys.ResumeStdioAfterReload); + } + + if (!useHttp && isRunning) + { + // Stop only the stdio bridge; leave HTTP untouched if it is running concurrently. + MCPServiceLocator.TransportManager.StopAsync(TransportMode.Stdio); + } + } + catch (Exception ex) + { + McpLog.Warn($"Failed to persist stdio reload flag: {ex.Message}"); + } + } + + private static void OnAfterAssemblyReload() + { + bool resume = false; + try + { + resume = EditorPrefs.GetBool(EditorPrefKeys.ResumeStdioAfterReload, false); + bool useHttp = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true); + resume = resume && !useHttp; + if (resume) + { + EditorPrefs.DeleteKey(EditorPrefKeys.ResumeStdioAfterReload); + } + } + catch (Exception ex) + { + McpLog.Warn($"Failed to read stdio reload flag: {ex.Message}"); + } + + if (!resume) + { + return; + } + + // Restart via TransportManager so state stays in sync; if it fails (port busy), rely on UI to retry. + TryStartBridgeImmediate(); + } + + private static void TryStartBridgeImmediate() + { + try + { + MCPServiceLocator.TransportManager.StartAsync(TransportMode.Stdio); + MCPForUnity.Editor.Windows.MCPForUnityEditorWindow.RequestHealthVerification(); + } + catch (Exception ex) + { + McpLog.Warn($"Failed to resume stdio bridge after reload: {ex.Message}"); + } + } + } +} diff --git a/MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs.meta b/MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs.meta new file mode 100644 index 000000000..d4e43fa9e --- /dev/null +++ b/MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6e603c72a87974cf5b495cd683165fbf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/MCPForUnity/Editor/Services/Transport/TransportManager.cs b/MCPForUnity/Editor/Services/Transport/TransportManager.cs index 7a6afe921..af3492ae2 100644 --- a/MCPForUnity/Editor/Services/Transport/TransportManager.cs +++ b/MCPForUnity/Editor/Services/Transport/TransportManager.cs @@ -10,8 +10,10 @@ namespace MCPForUnity.Editor.Services.Transport /// public class TransportManager { - private IMcpTransportClient _active; - private TransportMode? _activeMode; + private IMcpTransportClient _httpClient; + private IMcpTransportClient _stdioClient; + private TransportState _httpState = TransportState.Disconnected("http"); + private TransportState _stdioState = TransportState.Disconnected("stdio"); private Func _webSocketFactory; private Func _stdioFactory; @@ -22,8 +24,8 @@ public TransportManager() () => new StdioTransportClient()); } - public IMcpTransportClient ActiveTransport => _active; - public TransportMode? ActiveMode => _activeMode; + public IMcpTransportClient ActiveTransport => null; // Deprecated single-transport accessor + public TransportMode? ActiveMode => null; // Deprecated single-transport accessor public void Configure( Func webSocketFactory, @@ -33,68 +35,90 @@ public void Configure( _stdioFactory = stdioFactory ?? throw new ArgumentNullException(nameof(stdioFactory)); } - public async Task StartAsync(TransportMode mode) + private IMcpTransportClient GetOrCreateClient(TransportMode mode) { - await StopAsync(); - - IMcpTransportClient next = mode switch + return mode switch { - TransportMode.Stdio => _stdioFactory(), - TransportMode.Http => _webSocketFactory(), - _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "Unsupported transport mode") - } ?? throw new InvalidOperationException($"Factory returned null for transport mode {mode}"); + TransportMode.Http => _httpClient ??= _webSocketFactory(), + TransportMode.Stdio => _stdioClient ??= _stdioFactory(), + _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "Unsupported transport mode"), + }; + } + + public async Task StartAsync(TransportMode mode) + { + IMcpTransportClient client = GetOrCreateClient(mode); - bool started = await next.StartAsync(); + bool started = await client.StartAsync(); if (!started) { - await next.StopAsync(); - _active = null; - _activeMode = null; + await client.StopAsync(); + UpdateState(mode, TransportState.Disconnected(mode.ToString().ToLowerInvariant(), "Failed to start")); return false; } - _active = next; - _activeMode = mode; + UpdateState(mode, client.State ?? TransportState.Connected(client.TransportName)); return true; } - public async Task StopAsync() + public async Task StopAsync(TransportMode? mode = null) { - if (_active != null) + async Task StopClient(IMcpTransportClient client, TransportMode clientMode) + { + if (client == null) return; + try { await client.StopAsync(); } + catch (Exception ex) { McpLog.Warn($"Error while stopping transport {client.TransportName}: {ex.Message}"); } + finally { UpdateState(clientMode, TransportState.Disconnected(client.TransportName)); } + } + + if (mode == null) + { + await StopClient(_httpClient, TransportMode.Http); + await StopClient(_stdioClient, TransportMode.Stdio); + return; + } + + if (mode == TransportMode.Http) + { + await StopClient(_httpClient, TransportMode.Http); + } + else { - try - { - await _active.StopAsync(); - } - catch (Exception ex) - { - McpLog.Warn($"Error while stopping transport {_active.TransportName}: {ex.Message}"); - } - finally - { - _active = null; - _activeMode = null; - } + await StopClient(_stdioClient, TransportMode.Stdio); } } - public async Task VerifyAsync() + public async Task VerifyAsync(TransportMode mode) { - if (_active == null) + IMcpTransportClient client = mode == TransportMode.Http ? _httpClient : _stdioClient; + if (client == null) { return false; } - return await _active.VerifyAsync(); + + bool ok = await client.VerifyAsync(); + var state = client.State ?? TransportState.Disconnected(client.TransportName, "No state reported"); + UpdateState(mode, state); + return ok; } - public TransportState GetState() + public TransportState GetState(TransportMode mode) { - if (_active == null) + return mode == TransportMode.Http ? _httpState : _stdioState; + } + + public bool IsRunning(TransportMode mode) => GetState(mode).IsConnected; + + private void UpdateState(TransportMode mode, TransportState state) + { + if (mode == TransportMode.Http) { - return TransportState.Disconnected(_activeMode?.ToString()?.ToLowerInvariant() ?? "unknown", "Transport not started"); + _httpState = state; + } + else + { + _stdioState = state; } - - return _active.State ?? TransportState.Disconnected(_active.TransportName, "No state reported"); } } diff --git a/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs b/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs index 5cc1585a6..ab127b487 100644 --- a/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs +++ b/MCPForUnity/Editor/Services/Transport/Transports/StdioBridgeHost.cs @@ -57,7 +57,6 @@ public static class StdioBridgeHost private static int mainThreadId; private static int currentUnityPort = 6400; private static bool isAutoConnectMode = false; - private static bool shouldRestartAfterReload = false; private const ulong MaxFrameBytes = 64UL * 1024 * 1024; private const int FrameIOTimeoutMs = 30000; @@ -162,8 +161,6 @@ static StdioBridgeHost() } } EditorApplication.quitting += Stop; - AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload; - AssemblyReloadEvents.afterAssemblyReload += OnAfterAssemblyReload; EditorApplication.playModeStateChanged += _ => { if (ShouldAutoStartBridge()) @@ -406,10 +403,6 @@ public static void Start() listenerTask = Task.Run(() => ListenerLoopAsync(cts.Token)); CommandRegistry.Initialize(); EditorApplication.update += ProcessCommands; - try { AssemblyReloadEvents.beforeAssemblyReload -= OnBeforeAssemblyReload; } catch { } - try { AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload; } catch { } - try { AssemblyReloadEvents.afterAssemblyReload -= OnAfterAssemblyReload; } catch { } - try { AssemblyReloadEvents.afterAssemblyReload += OnAfterAssemblyReload; } catch { } try { EditorApplication.quitting -= Stop; } catch { } try { EditorApplication.quitting += Stop; } catch { } heartbeatSeq++; @@ -470,8 +463,6 @@ public static void Stop() } try { EditorApplication.update -= ProcessCommands; } catch { } - try { AssemblyReloadEvents.beforeAssemblyReload -= OnBeforeAssemblyReload; } catch { } - try { AssemblyReloadEvents.afterAssemblyReload -= OnAfterAssemblyReload; } catch { } try { EditorApplication.quitting -= Stop; } catch { } try @@ -1023,47 +1014,6 @@ private static bool IsValidJson(string text) return false; } - private static void OnBeforeAssemblyReload() - { - if (isRunning) - { - shouldRestartAfterReload = true; - } - try { Stop(); } catch { } - } - - private static void OnAfterAssemblyReload() - { - WriteHeartbeat(false, "idle"); - LogBreadcrumb("Idle"); - bool shouldResume = ShouldAutoStartBridge() || shouldRestartAfterReload; - if (shouldRestartAfterReload) - { - shouldRestartAfterReload = false; - } - if (!shouldResume) - { - return; - } - - // If we're not compiling, try to bring the bridge up immediately to avoid depending on editor focus. - if (!IsCompiling()) - { - try - { - Start(); - return; // Successful immediate start; no need to schedule a delayed retry - } - catch (Exception ex) - { - // Fall through to delayed retry if immediate start fails - McpLog.Warn($"Immediate STDIO bridge restart after reload failed: {ex.Message}"); - } - } - - // Fallback path when compiling or if immediate start failed - ScheduleInitRetry(); - } private static void WriteHeartbeat(bool reloading, string reason = null) { From 9e11b1dab8ec13abe4b520b9c06c59363fd033ec Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Thu, 27 Nov 2025 19:08:48 -0400 Subject: [PATCH 12/14] Fix ActiveMode to return resolved transport mode instead of preferred mode The ActiveMode property now calls ResolvePreferredMode() to return the actual active transport mode rather than just the preferred mode setting. --- MCPForUnity/Editor/Services/BridgeControlService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MCPForUnity/Editor/Services/BridgeControlService.cs b/MCPForUnity/Editor/Services/BridgeControlService.cs index 3e18b67cf..4c18da046 100644 --- a/MCPForUnity/Editor/Services/BridgeControlService.cs +++ b/MCPForUnity/Editor/Services/BridgeControlService.cs @@ -75,7 +75,7 @@ public int CurrentPort } public bool IsAutoConnectMode => StdioBridgeHost.IsAutoConnectMode(); - public TransportMode? ActiveMode => _preferredMode; + public TransportMode? ActiveMode => ResolvePreferredMode(); public async Task StartAsync() { From df14d84e2574a2a931769766d0cdc362af49a53d Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Thu, 27 Nov 2025 19:13:10 -0400 Subject: [PATCH 13/14] Minor improvements for stdio bridge - Consolidated the !useHttp && isRunning checks into a single shouldResume flag. - Wrapped the fire-and-forget StopAsync in a continuation that logs faults (matching the HTTP handler pattern). - Wrapped StartAsync in a continuation that logs failures and only triggers the health check on success. --- .../Services/StdioBridgeReloadHandler.cs | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs b/MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs index 98921c431..320203514 100644 --- a/MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs +++ b/MCPForUnity/Editor/Services/StdioBridgeReloadHandler.cs @@ -26,20 +26,26 @@ private static void OnBeforeAssemblyReload() // Only persist resume intent when stdio is the active transport and the bridge is running. bool useHttp = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true); bool isRunning = MCPServiceLocator.TransportManager.IsRunning(TransportMode.Stdio); - if (!useHttp && isRunning) + bool shouldResume = !useHttp && isRunning; + + if (shouldResume) { EditorPrefs.SetBool(EditorPrefKeys.ResumeStdioAfterReload, true); + + // Stop only the stdio bridge; leave HTTP untouched if it is running concurrently. + var stopTask = MCPServiceLocator.TransportManager.StopAsync(TransportMode.Stdio); + stopTask.ContinueWith(t => + { + if (t.IsFaulted && t.Exception != null) + { + McpLog.Warn($"Error stopping stdio bridge before reload: {t.Exception.GetBaseException()?.Message}"); + } + }, System.Threading.Tasks.TaskScheduler.Default); } else { EditorPrefs.DeleteKey(EditorPrefKeys.ResumeStdioAfterReload); } - - if (!useHttp && isRunning) - { - // Stop only the stdio bridge; leave HTTP untouched if it is running concurrently. - MCPServiceLocator.TransportManager.StopAsync(TransportMode.Stdio); - } } catch (Exception ex) { @@ -76,15 +82,23 @@ private static void OnAfterAssemblyReload() private static void TryStartBridgeImmediate() { - try + var startTask = MCPServiceLocator.TransportManager.StartAsync(TransportMode.Stdio); + startTask.ContinueWith(t => { - MCPServiceLocator.TransportManager.StartAsync(TransportMode.Stdio); + if (t.IsFaulted) + { + var baseEx = t.Exception?.GetBaseException(); + McpLog.Warn($"Failed to resume stdio bridge after reload: {baseEx?.Message}"); + return; + } + if (!t.Result) + { + McpLog.Warn("Failed to resume stdio bridge after domain reload"); + return; + } + MCPForUnity.Editor.Windows.MCPForUnityEditorWindow.RequestHealthVerification(); - } - catch (Exception ex) - { - McpLog.Warn($"Failed to resume stdio bridge after reload: {ex.Message}"); - } + }, System.Threading.Tasks.TaskScheduler.Default); } } } From caf89eec713945e9904d7bd3d82c251d3473830d Mon Sep 17 00:00:00 2001 From: Marcus Sanatan Date: Thu, 27 Nov 2025 19:15:11 -0400 Subject: [PATCH 14/14] Refactor TransportManager to use switch expressions and improve error handling - Replace if-else chains with switch expressions for better readability and exhaustiveness checking - Add GetClient() helper method to centralize client retrieval logic - Wrap StopAsync in try-catch to log failures when stopping a failed transport - Use client.TransportName instead of mode.ToString() for consistent naming in error messages --- .../Services/Transport/TransportManager.cs | 45 ++++++++++++++----- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/MCPForUnity/Editor/Services/Transport/TransportManager.cs b/MCPForUnity/Editor/Services/Transport/TransportManager.cs index af3492ae2..d221ab831 100644 --- a/MCPForUnity/Editor/Services/Transport/TransportManager.cs +++ b/MCPForUnity/Editor/Services/Transport/TransportManager.cs @@ -45,6 +45,16 @@ private IMcpTransportClient GetOrCreateClient(TransportMode mode) }; } + private IMcpTransportClient GetClient(TransportMode mode) + { + return mode switch + { + TransportMode.Http => _httpClient, + TransportMode.Stdio => _stdioClient, + _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "Unsupported transport mode"), + }; + } + public async Task StartAsync(TransportMode mode) { IMcpTransportClient client = GetOrCreateClient(mode); @@ -52,8 +62,15 @@ public async Task StartAsync(TransportMode mode) bool started = await client.StartAsync(); if (!started) { - await client.StopAsync(); - UpdateState(mode, TransportState.Disconnected(mode.ToString().ToLowerInvariant(), "Failed to start")); + try + { + await client.StopAsync(); + } + catch (Exception ex) + { + McpLog.Warn($"Error while stopping transport {client.TransportName}: {ex.Message}"); + } + UpdateState(mode, TransportState.Disconnected(client.TransportName, "Failed to start")); return false; } @@ -90,7 +107,7 @@ async Task StopClient(IMcpTransportClient client, TransportMode clientMode) public async Task VerifyAsync(TransportMode mode) { - IMcpTransportClient client = mode == TransportMode.Http ? _httpClient : _stdioClient; + IMcpTransportClient client = GetClient(mode); if (client == null) { return false; @@ -104,20 +121,28 @@ public async Task VerifyAsync(TransportMode mode) public TransportState GetState(TransportMode mode) { - return mode == TransportMode.Http ? _httpState : _stdioState; + return mode switch + { + TransportMode.Http => _httpState, + TransportMode.Stdio => _stdioState, + _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "Unsupported transport mode"), + }; } public bool IsRunning(TransportMode mode) => GetState(mode).IsConnected; private void UpdateState(TransportMode mode, TransportState state) { - if (mode == TransportMode.Http) - { - _httpState = state; - } - else + switch (mode) { - _stdioState = state; + case TransportMode.Http: + _httpState = state; + break; + case TransportMode.Stdio: + _stdioState = state; + break; + default: + throw new ArgumentOutOfRangeException(nameof(mode), mode, "Unsupported transport mode"); } } }