Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions MCPForUnity/Editor/Clients/Configurators/ClineConfigurator.cs
Original file line number Diff line number Diff line change
@@ -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<string> GetInstallationSteps() => new List<string>
{
"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"
};
}
}
11 changes: 11 additions & 0 deletions MCPForUnity/Editor/Clients/Configurators/ClineConfigurator.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 11 additions & 37 deletions MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,47 +80,21 @@ protected bool UrlsEqual(string a, string b)
}

/// <summary>
/// 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.
/// </summary>
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();
Comment on lines +89 to 90
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Expected package source for validation no longer honors Git URL override, contrary to the comment.

This used to return EditorPrefKeys.GitUrlOverride directly, so validation matched configs using a local path or git URL. Now it always uses GetMcpServerPackageSource(), which appears version‑only and unaware of the git override, so projects intentionally using an override will be reported as mismatched. If validation should still honor overrides, either reintroduce the override check here or have GetMcpServerPackageSource() handle it. The new comment also claims explicit overrides are included, which doesn’t match the current behavior.

}

/// <summary>
/// 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"
/// </summary>
protected static bool IsBetaPackageSource(string packageSource)
Expand All @@ -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;

Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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
{
Expand Down
1 change: 0 additions & 1 deletion MCPForUnity/Editor/Constants/EditorPrefKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
61 changes: 37 additions & 24 deletions MCPForUnity/Editor/Helpers/AssetPathUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}";
}

Expand All @@ -245,32 +252,29 @@ public static (string uvxPath, string fromUrl, string packageName) GetUvxCommand

/// <summary>
/// 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.
/// </summary>
/// <param name="quoteFromPath">Whether to quote the --from path (needed for command-line strings, not for arg lists)</param>
/// <returns>The package source arguments (e.g., "--prerelease explicit --from mcpforunityserver>=0.0.0a0")</returns>
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(gitUrlOverride, packageSource, quoteFromPath);
}

/// <summary>
/// Thread-safe overload that accepts pre-captured values.
/// Use this when calling from background threads.
/// </summary>
/// <param name="useBetaServer">Pre-captured value from EditorConfigurationCache.Instance.UseBetaServer</param>
/// <param name="gitUrlOverride">Pre-captured value from EditorPrefs GitUrlOverride</param>
/// <param name="packageSource">Pre-captured value from GetMcpServerPackageSource()</param>
/// <param name="quoteFromPath">Whether to quote the --from path</param>
public static string GetBetaServerFromArgs(bool useBetaServer, string gitUrlOverride, string packageSource, bool quoteFromPath = false)
public static string GetBetaServerFromArgs(string gitUrlOverride, string packageSource, bool quoteFromPath = false)
{
// Explicit override (local path, git URL, etc.) always wins
if (!string.IsNullOrEmpty(gitUrlOverride))
Expand All @@ -279,8 +283,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).
Expand All @@ -300,28 +306,25 @@ public static string GetBetaServerFromArgs(bool useBetaServer, string gitUrlOver

/// <summary>
/// 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.
/// </summary>
/// <returns>List of arguments to add to uvx command</returns>
public static System.Collections.Generic.IList<string> 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(gitUrlOverride, packageSource);
}

/// <summary>
/// Thread-safe overload that accepts pre-captured values.
/// Use this when calling from background threads.
/// </summary>
/// <param name="useBetaServer">Pre-captured value from EditorConfigurationCache.Instance.UseBetaServer</param>
/// <param name="gitUrlOverride">Pre-captured value from EditorPrefs GitUrlOverride</param>
/// <param name="packageSource">Pre-captured value from GetMcpServerPackageSource()</param>
public static System.Collections.Generic.IList<string> GetBetaServerFromArgsList(bool useBetaServer, string gitUrlOverride, string packageSource)
public static System.Collections.Generic.IList<string> GetBetaServerFromArgsList(string gitUrlOverride, string packageSource)
{
var args = new System.Collections.Generic.List<string>();

Expand All @@ -333,8 +336,10 @@ public static System.Collections.Generic.IList<string> 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");
Expand Down Expand Up @@ -472,18 +477,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);
}
}
}
14 changes: 12 additions & 2 deletions MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>(StringComparer.OrdinalIgnoreCase) { "url", "serverUrl" };
urlPropsToRemove.Remove(httpProperty);
Expand Down Expand Up @@ -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
{
Expand All @@ -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");
}
Expand Down
Loading