From 4fee827443aec12e332510920c82d6f4dc217511 Mon Sep 17 00:00:00 2001 From: dsarno Date: Wed, 11 Feb 2026 08:36:57 -0800 Subject: [PATCH 1/2] Add Cline configurator and auto-select server channel by package version --- .../Configurators/ClineConfigurator.cs | 29 +++++++++ .../Configurators/ClineConfigurator.cs.meta | 11 ++++ .../Clients/McpClientConfiguratorBase.cs | 48 ++++----------- .../Editor/Constants/EditorPrefKeys.cs | 1 - .../Editor/Helpers/AssetPathUtility.cs | 59 ++++++++++++------- .../Editor/Helpers/ConfigJsonBuilder.cs | 14 ++++- .../Services/EditorConfigurationCache.cs | 41 ------------- .../Components/Advanced/McpAdvancedSection.cs | 19 ------ .../Advanced/McpAdvancedSection.uxml | 5 -- .../ClientConfig/McpClientConfigSection.cs | 34 ++--------- .../Windows/EditorPrefs/EditorPrefsWindow.cs | 1 - .../Editor/Windows/MCPForUnityEditorWindow.cs | 37 ++++++------ 12 files changed, 121 insertions(+), 178 deletions(-) create mode 100644 MCPForUnity/Editor/Clients/Configurators/ClineConfigurator.cs create mode 100644 MCPForUnity/Editor/Clients/Configurators/ClineConfigurator.cs.meta diff --git a/MCPForUnity/Editor/Clients/Configurators/ClineConfigurator.cs b/MCPForUnity/Editor/Clients/Configurators/ClineConfigurator.cs new file mode 100644 index 000000000..4cce6cc8c --- /dev/null +++ b/MCPForUnity/Editor/Clients/Configurators/ClineConfigurator.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.IO; +using MCPForUnity.Editor.Models; + +namespace MCPForUnity.Editor.Clients.Configurators +{ + public class ClineConfigurator : JsonFileMcpConfigurator + { + public ClineConfigurator() : base(new McpClient + { + name = "Cline", + windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"), + macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"), + linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"), + DefaultUnityFields = { { "disabled", false }, { "autoApprove", new object[] { } } } + }) + { } + + public override IList GetInstallationSteps() => new List + { + "Open Cline in VS Code", + "Click the MCP Servers icon in the Cline pane", + "Go to Configure tab and click 'Configure MCP Servers'\nOR open the config file at the path above", + "Paste the configuration JSON into the mcpServers object", + "Save and restart VS Code" + }; + } +} diff --git a/MCPForUnity/Editor/Clients/Configurators/ClineConfigurator.cs.meta b/MCPForUnity/Editor/Clients/Configurators/ClineConfigurator.cs.meta new file mode 100644 index 000000000..77045ec75 --- /dev/null +++ b/MCPForUnity/Editor/Clients/Configurators/ClineConfigurator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6b8abf0951c7413d9ff97a053b0adf2d +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 index 42656958f..021c14ce4 100644 --- a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs +++ b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs @@ -80,39 +80,13 @@ protected bool UrlsEqual(string a, string b) } /// - /// Gets the expected package source for validation, accounting for beta mode. + /// Gets the expected package source for validation based on the installed package version. /// This should match what Configure() would actually use for the --from argument. /// MUST be called from the main thread due to EditorPrefs access. /// protected static string GetExpectedPackageSourceForValidation() { - // Check for explicit override first - string gitUrlOverride = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, ""); - if (!string.IsNullOrEmpty(gitUrlOverride)) - { - return gitUrlOverride; - } - - // Check beta mode using the same logic as GetUseBetaServerWithDynamicDefault - // (bypass cache to ensure fresh read) - bool useBetaServer; - bool hasPrefKey = EditorPrefs.HasKey(EditorPrefKeys.UseBetaServer); - if (hasPrefKey) - { - useBetaServer = EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, false); - } - else - { - // Dynamic default based on package version - useBetaServer = AssetPathUtility.IsPreReleaseVersion(); - } - - if (useBetaServer) - { - return "mcpforunityserver>=0.0.0a0"; - } - - // Standard mode uses exact version from package.json + // Includes explicit override, stable pin, or prerelease range depending on package version. return AssetPathUtility.GetMcpServerPackageSource(); } @@ -120,7 +94,7 @@ protected static string GetExpectedPackageSourceForValidation() /// Checks if a package source string represents a beta/prerelease version. /// Beta versions include: /// - PyPI beta: "mcpforunityserver==9.4.0b20250203..." (contains 'b' before timestamp) - /// - PyPI prerelease range: "mcpforunityserver>=0.0.0a0" (used when beta mode is enabled) + /// - PyPI prerelease range: "mcpforunityserver>=0.0.0a0" (used for prerelease package builds) /// - Git beta branch: contains "@beta" or "-beta" /// protected static bool IsBetaPackageSource(string packageSource) @@ -133,7 +107,7 @@ protected static bool IsBetaPackageSource(string packageSource) if (System.Text.RegularExpressions.Regex.IsMatch(packageSource, @"==\d+\.\d+\.\d+b\d+")) return true; - // PyPI prerelease range: >=0.0.0a0 (used when "Use Beta Server" is enabled in Unity settings) + // PyPI prerelease range: >=0.0.0a0 (used for prerelease package builds) if (packageSource.Contains(">=0.0.0a0", StringComparison.OrdinalIgnoreCase)) return true; @@ -266,12 +240,12 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true) if (configuredIsBeta && !expectedIsBeta) { hasVersionMismatch = true; - mismatchReason = "Configured for beta server, but 'Use Beta Server' is disabled in Advanced settings."; + mismatchReason = "Configured for prerelease server, but this package is stable. Re-configure to switch to stable."; } else if (!configuredIsBeta && expectedIsBeta) { hasVersionMismatch = true; - mismatchReason = "Configured for stable server, but 'Use Beta Server' is enabled in Advanced settings."; + mismatchReason = "Configured for stable server, but this package is prerelease. Re-configure to switch to prerelease."; } else { @@ -449,12 +423,12 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true) if (configuredIsBeta && !expectedIsBeta) { hasVersionMismatch = true; - mismatchReason = "Configured for beta server, but 'Use Beta Server' is disabled in Advanced settings."; + mismatchReason = "Configured for prerelease server, but this package is stable. Re-configure to switch to stable."; } else if (!configuredIsBeta && expectedIsBeta) { hasVersionMismatch = true; - mismatchReason = "Configured for stable server, but 'Use Beta Server' is enabled in Advanced settings."; + mismatchReason = "Configured for stable server, but this package is prerelease. Re-configure to switch to prerelease."; } else { @@ -579,7 +553,7 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true) string claudePath = MCPServiceLocator.Paths.GetClaudeCliPath(); RuntimePlatform platform = Application.platform; bool isRemoteScope = HttpEndpointUtility.IsRemoteScope(); - // Get expected package source considering beta mode (matches what Register() would use) + // Get expected package source for the installed package version (matches what Register() would use) string expectedPackageSource = GetExpectedPackageSourceForValidation(); return CheckStatusWithProjectDir(projectDir, useHttpTransport, claudePath, platform, isRemoteScope, expectedPackageSource, attemptAutoRewrite); } @@ -679,11 +653,11 @@ internal McpStatus CheckStatusWithProjectDir( if (configuredIsBeta && !expectedIsBeta) { - mismatchReason = "Configured for beta server, but 'Use Beta Server' is disabled in Advanced settings."; + mismatchReason = "Configured for prerelease server, but this package is stable. Re-configure to switch to stable."; } else if (!configuredIsBeta && expectedIsBeta) { - mismatchReason = "Configured for stable server, but 'Use Beta Server' is enabled in Advanced settings."; + mismatchReason = "Configured for stable server, but this package is prerelease. Re-configure to switch to prerelease."; } else { diff --git a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs index 5f99d3e5a..5ef737fb3 100644 --- a/MCPForUnity/Editor/Constants/EditorPrefKeys.cs +++ b/MCPForUnity/Editor/Constants/EditorPrefKeys.cs @@ -29,7 +29,6 @@ internal static class EditorPrefKeys internal const string WebSocketUrlOverride = "MCPForUnity.WebSocketUrl"; internal const string GitUrlOverride = "MCPForUnity.GitUrlOverride"; internal const string DevModeForceServerRefresh = "MCPForUnity.DevModeForceServerRefresh"; - internal const string UseBetaServer = "MCPForUnity.UseBetaServer"; internal const string ProjectScopedToolsLocalHttp = "MCPForUnity.ProjectScopedTools.LocalHttp"; internal const string PackageDeploySourcePath = "MCPForUnity.PackageDeploy.SourcePath"; diff --git a/MCPForUnity/Editor/Helpers/AssetPathUtility.cs b/MCPForUnity/Editor/Helpers/AssetPathUtility.cs index fc450bada..517b1900b 100644 --- a/MCPForUnity/Editor/Helpers/AssetPathUtility.cs +++ b/MCPForUnity/Editor/Helpers/AssetPathUtility.cs @@ -220,6 +220,13 @@ public static string GetMcpServerPackageSource() return "mcpforunityserver"; } + // Package.json uses semver prerelease tags (e.g., 9.4.5-beta.1) that are not valid + // PEP 440 pins for uvx. Use the beta prerelease range instead of a pinned prerelease. + if (IsSemVerPreRelease(version)) + { + return "mcpforunityserver>=0.0.0a0"; + } + return $"mcpforunityserver=={version}"; } @@ -245,9 +252,9 @@ public static (string uvxPath, string fromUrl, string packageName) GetUvxCommand /// /// Builds the uvx package source arguments for the MCP server. - /// Handles beta server mode (prerelease from PyPI) vs standard mode (pinned version or override). + /// Handles prerelease package mode (prerelease from PyPI) vs stable mode (pinned version or override). /// Centralizes the prerelease logic to avoid duplication between HTTP and stdio transports. - /// Priority: explicit fromUrl override > beta server mode > default package. + /// Priority: explicit fromUrl override > package-version-driven prerelease mode > stable pinned package. /// NOTE: This overload reads from EditorPrefs/cache and MUST be called from the main thread. /// For background threads, use the overload that accepts pre-captured parameters. /// @@ -255,18 +262,16 @@ public static (string uvxPath, string fromUrl, string packageName) GetUvxCommand /// The package source arguments (e.g., "--prerelease explicit --from mcpforunityserver>=0.0.0a0") public static string GetBetaServerFromArgs(bool quoteFromPath = false) { - // Read values from cache/EditorPrefs on main thread - bool useBetaServer = Services.EditorConfigurationCache.Instance.UseBetaServer; string gitUrlOverride = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, ""); string packageSource = GetMcpServerPackageSource(); - return GetBetaServerFromArgs(useBetaServer, gitUrlOverride, packageSource, quoteFromPath); + return GetBetaServerFromArgs(useBetaServer: false, gitUrlOverride, packageSource, quoteFromPath); } /// /// Thread-safe overload that accepts pre-captured values. /// Use this when calling from background threads. /// - /// Pre-captured value from EditorConfigurationCache.Instance.UseBetaServer + /// Deprecated. Ignored; prerelease mode is determined from packageSource. /// Pre-captured value from EditorPrefs GitUrlOverride /// Pre-captured value from GetMcpServerPackageSource() /// Whether to quote the --from path @@ -279,8 +284,10 @@ public static string GetBetaServerFromArgs(bool useBetaServer, string gitUrlOver return $"--from {fromValue}"; } - // Beta server mode: use prerelease from PyPI - if (useBetaServer) + bool usePrereleaseRange = string.Equals(packageSource, "mcpforunityserver>=0.0.0a0", StringComparison.OrdinalIgnoreCase); + + // Prerelease package mode: use prerelease from PyPI. + if (usePrereleaseRange) { // Use --prerelease explicit with version specifier to only get prereleases of our package, // not of dependencies (which can be broken on PyPI). @@ -300,25 +307,23 @@ public static string GetBetaServerFromArgs(bool useBetaServer, string gitUrlOver /// /// Builds the uvx package source arguments as a list (for JSON config builders). - /// Priority: explicit fromUrl override > beta server mode > default package. + /// Priority: explicit fromUrl override > package-version-driven prerelease mode > stable pinned package. /// NOTE: This overload reads from EditorPrefs/cache and MUST be called from the main thread. /// For background threads, use the overload that accepts pre-captured parameters. /// /// List of arguments to add to uvx command public static System.Collections.Generic.IList GetBetaServerFromArgsList() { - // Read values from cache/EditorPrefs on main thread - bool useBetaServer = Services.EditorConfigurationCache.Instance.UseBetaServer; string gitUrlOverride = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, ""); string packageSource = GetMcpServerPackageSource(); - return GetBetaServerFromArgsList(useBetaServer, gitUrlOverride, packageSource); + return GetBetaServerFromArgsList(useBetaServer: false, gitUrlOverride, packageSource); } /// /// Thread-safe overload that accepts pre-captured values. /// Use this when calling from background threads. /// - /// Pre-captured value from EditorConfigurationCache.Instance.UseBetaServer + /// Deprecated. Ignored; prerelease mode is determined from packageSource. /// Pre-captured value from EditorPrefs GitUrlOverride /// Pre-captured value from GetMcpServerPackageSource() public static System.Collections.Generic.IList GetBetaServerFromArgsList(bool useBetaServer, string gitUrlOverride, string packageSource) @@ -333,8 +338,10 @@ public static System.Collections.Generic.IList GetBetaServerFromArgsList return args; } - // Beta server mode: use prerelease from PyPI - if (useBetaServer) + bool usePrereleaseRange = string.Equals(packageSource, "mcpforunityserver>=0.0.0a0", StringComparison.OrdinalIgnoreCase); + + // Prerelease package mode: use prerelease from PyPI. + if (usePrereleaseRange) { args.Add("--prerelease"); args.Add("explicit"); @@ -472,18 +479,26 @@ public static bool IsPreReleaseVersion() if (string.IsNullOrEmpty(version) || version == "unknown") return false; - // Check for common prerelease indicators in semver format - // e.g., "9.3.0-beta.1", "9.3.0-alpha", "9.3.0-rc.2", "9.3.0-preview" - return version.Contains("-beta", StringComparison.OrdinalIgnoreCase) || - version.Contains("-alpha", StringComparison.OrdinalIgnoreCase) || - version.Contains("-rc", StringComparison.OrdinalIgnoreCase) || - version.Contains("-preview", StringComparison.OrdinalIgnoreCase) || - version.Contains("-pre", StringComparison.OrdinalIgnoreCase); + return IsSemVerPreRelease(version); } catch { return false; } } + + private static bool IsSemVerPreRelease(string version) + { + if (string.IsNullOrEmpty(version)) + return false; + + // Common semver prerelease indicators: + // e.g., "9.3.0-beta.1", "9.3.0-alpha", "9.3.0-rc.2", "9.3.0-preview" + return version.Contains("-beta", StringComparison.OrdinalIgnoreCase) || + version.Contains("-alpha", StringComparison.OrdinalIgnoreCase) || + version.Contains("-rc", StringComparison.OrdinalIgnoreCase) || + version.Contains("-preview", StringComparison.OrdinalIgnoreCase) || + version.Contains("-pre", StringComparison.OrdinalIgnoreCase); + } } } diff --git a/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs b/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs index 938d33c26..815477232 100644 --- a/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs +++ b/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs @@ -52,6 +52,7 @@ private static void PopulateUnityNode(JObject unity, string uvPath, McpClient cl bool prefValue = EditorConfigurationCache.Instance.UseHttpTransport; bool clientSupportsHttp = client?.SupportsHttpTransport != false; bool useHttpTransport = clientSupportsHttp && prefValue; + bool isCline = client?.name == "Cline"; string httpProperty = string.IsNullOrEmpty(client?.HttpUrlProperty) ? "url" : client.HttpUrlProperty; var urlPropsToRemove = new HashSet(StringComparer.OrdinalIgnoreCase) { "url", "serverUrl" }; urlPropsToRemove.Remove(httpProperty); @@ -100,6 +101,11 @@ private static void PopulateUnityNode(JObject unity, string uvPath, McpClient cl { unity["type"] = "http"; } + // Cline expects streamableHttp for HTTP endpoints. + else if (isCline) + { + unity["type"] = "streamableHttp"; + } } else { @@ -119,10 +125,14 @@ private static void PopulateUnityNode(JObject unity, string uvPath, McpClient cl { unity["type"] = "stdio"; } + else if (isCline) + { + unity["type"] = "stdio"; + } } - // Remove type for non-VSCode clients (except Claude Code which needs it) - if (!isVSCode && client?.name != "Claude Code" && unity["type"] != null) + // Remove type for non-VSCode clients (except clients that explicitly require it) + if (!isVSCode && client?.name != "Claude Code" && !isCline && unity["type"] != null) { unity.Remove("type"); } diff --git a/MCPForUnity/Editor/Services/EditorConfigurationCache.cs b/MCPForUnity/Editor/Services/EditorConfigurationCache.cs index 3f8f04a9f..94f6a397a 100644 --- a/MCPForUnity/Editor/Services/EditorConfigurationCache.cs +++ b/MCPForUnity/Editor/Services/EditorConfigurationCache.cs @@ -48,7 +48,6 @@ public static EditorConfigurationCache Instance // Cached values - most frequently read private bool _useHttpTransport; private bool _debugLogs; - private bool _useBetaServer; private bool _devModeForceServerRefresh; private string _uvxPathOverride; private string _gitUrlOverride; @@ -70,12 +69,6 @@ public static EditorConfigurationCache Instance /// public bool DebugLogs => _debugLogs; - /// - /// Whether to use the beta server channel. - /// Default: true - /// - public bool UseBetaServer => _useBetaServer; - /// /// Whether to force server refresh in dev mode (--no-cache --refresh). /// Default: false @@ -124,23 +117,6 @@ public static EditorConfigurationCache Instance /// public int UnitySocketPort => _unitySocketPort; - /// - /// Gets UseBetaServer value with dynamic default based on package version. - /// If the pref hasn't been explicitly set, defaults to true for prerelease packages - /// (beta, alpha, rc, etc.) and false for stable releases. - /// - private static bool GetUseBetaServerWithDynamicDefault() - { - // If user has explicitly set the pref, use that value - if (EditorPrefs.HasKey(EditorPrefKeys.UseBetaServer)) - { - return EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, false); - } - - // Otherwise, default based on whether this is a prerelease package - return Helpers.AssetPathUtility.IsPreReleaseVersion(); - } - private EditorConfigurationCache() { Refresh(); @@ -154,7 +130,6 @@ public void Refresh() { _useHttpTransport = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true); _debugLogs = EditorPrefs.GetBool(EditorPrefKeys.DebugLogs, false); - _useBetaServer = GetUseBetaServerWithDynamicDefault(); _devModeForceServerRefresh = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false); _uvxPathOverride = EditorPrefs.GetString(EditorPrefKeys.UvxPathOverride, string.Empty); _gitUrlOverride = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, string.Empty); @@ -191,19 +166,6 @@ public void SetDebugLogs(bool value) } } - /// - /// Set UseBetaServer and update cache + EditorPrefs atomically. - /// - public void SetUseBetaServer(bool value) - { - if (_useBetaServer != value) - { - _useBetaServer = value; - EditorPrefs.SetBool(EditorPrefKeys.UseBetaServer, value); - OnConfigurationChanged?.Invoke(nameof(UseBetaServer)); - } - } - /// /// Set DevModeForceServerRefresh and update cache + EditorPrefs atomically. /// @@ -328,9 +290,6 @@ public void InvalidateKey(string keyName) case nameof(DebugLogs): _debugLogs = EditorPrefs.GetBool(EditorPrefKeys.DebugLogs, false); break; - case nameof(UseBetaServer): - _useBetaServer = GetUseBetaServerWithDynamicDefault(); - break; case nameof(DevModeForceServerRefresh): _devModeForceServerRefresh = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false); break; diff --git a/MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.cs b/MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.cs index 4957e7da8..26f610167 100644 --- a/MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.cs +++ b/MCPForUnity/Editor/Windows/Components/Advanced/McpAdvancedSection.cs @@ -26,7 +26,6 @@ public class McpAdvancedSection private Button clearGitUrlButton; private Toggle debugLogsToggle; private Toggle devModeForceRefreshToggle; - private Toggle useBetaServerToggle; private TextField deploySourcePath; private Button browseDeploySourceButton; private Button clearDeploySourceButton; @@ -43,7 +42,6 @@ public class McpAdvancedSection public event Action OnGitUrlChanged; public event Action OnHttpServerCommandUpdateRequested; public event Action OnTestConnectionRequested; - public event Action OnBetaModeChanged; public VisualElement Root { get; private set; } @@ -66,7 +64,6 @@ private void CacheUIElements() clearGitUrlButton = Root.Q