diff --git a/MCPForUnity/Editor/Clients/Configurators/RiderConfigurator.cs b/MCPForUnity/Editor/Clients/Configurators/RiderConfigurator.cs index 5db02acaf..ea1087ea4 100644 --- a/MCPForUnity/Editor/Clients/Configurators/RiderConfigurator.cs +++ b/MCPForUnity/Editor/Clients/Configurators/RiderConfigurator.cs @@ -10,9 +10,10 @@ public class RiderConfigurator : JsonFileMcpConfigurator public RiderConfigurator() : base(new McpClient { name = "Rider GitHub Copilot", - windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "JetBrains", "Rider", "mcp.json"), - macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "JetBrains", "Rider", "mcp.json"), - linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "JetBrains", "Rider", "mcp.json"), + // Rider GitHub Copilot uses github-copilot/intellij path under appropriate per-OS data directories + windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "github-copilot", "intellij", "mcp.json"), + macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "github-copilot", "intellij", "mcp.json"), + linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share", "github-copilot", "intellij", "mcp.json"), IsVsCodeLayout = true }) { } diff --git a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs index 897e7dfd8..5a0bb0317 100644 --- a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs +++ b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs @@ -540,9 +540,8 @@ private void RegisterWithCapturedValues( } else { - // Note: --reinstall is not supported by uvx, use --no-cache --refresh instead - string devFlags = shouldForceRefresh ? "--no-cache --refresh " : string.Empty; - args = $"mcp add --transport stdio UnityMCP -- \"{uvxPath}\" {devFlags}--from \"{gitUrl}\" {packageName}"; + string packageArgs = BuildClaudeUvxArgs(uvxPath, gitUrl, packageName, shouldForceRefresh); + args = $"mcp add --transport stdio UnityMCP -- {packageArgs}"; } // Remove any existing registrations - handle both "UnityMCP" and "unityMCP" (legacy) @@ -599,10 +598,9 @@ private void Register() else { var (uvxPath, gitUrl, packageName) = AssetPathUtility.GetUvxCommandParts(); - // Use central helper that checks both DevModeForceServerRefresh AND local path detection. - // Note: --reinstall is not supported by uvx, use --no-cache --refresh instead - string devFlags = AssetPathUtility.ShouldForceUvxRefresh() ? "--no-cache --refresh " : string.Empty; - args = $"mcp add --transport stdio UnityMCP -- \"{uvxPath}\" {devFlags}--from \"{gitUrl}\" {packageName}"; + bool shouldForceRefresh = AssetPathUtility.ShouldForceUvxRefresh(); + string packageArgs = BuildClaudeUvxArgs(uvxPath, gitUrl, packageName, shouldForceRefresh); + args = $"mcp add --transport stdio UnityMCP -- {packageArgs}"; } string projectDir = Path.GetDirectoryName(Application.dataPath); @@ -698,13 +696,12 @@ public override string GetManualSnippet() return "# Error: Configuration not available - check paths in Advanced Settings"; } - string packageSource = AssetPathUtility.GetMcpServerPackageSource(); - // Use central helper that checks both DevModeForceServerRefresh AND local path detection. - // Note: --reinstall is not supported by uvx, use --no-cache --refresh instead - string devFlags = AssetPathUtility.ShouldForceUvxRefresh() ? "--no-cache --refresh " : string.Empty; + var (_, gitUrl, packageName) = AssetPathUtility.GetUvxCommandParts(); + bool shouldForceRefresh = AssetPathUtility.ShouldForceUvxRefresh(); + string packageArgs = BuildClaudeUvxArgs(uvxPath, gitUrl, packageName, shouldForceRefresh); return "# Register the MCP server with Claude Code:\n" + - $"claude mcp add --transport stdio UnityMCP -- \"{uvxPath}\" {devFlags}--from \"{packageSource}\" mcp-for-unity\n\n" + + $"claude mcp add --transport stdio UnityMCP -- {packageArgs}\n\n" + "# Unregister the MCP server:\n" + "claude mcp remove UnityMCP\n\n" + "# List registered servers:\n" + @@ -763,5 +760,24 @@ private static string ExtractPackageSourceFromCliOutput(string cliOutput) return null; } + + /// + /// Builds the correct uvx package arguments for Claude CLI mcp add command. + /// IMPORTANT: We always need --from because the PyPI package name (mcpforunityserver) + /// differs from the executable name (mcp-for-unity). The uvx command format is: + /// uvx --from PACKAGE_SOURCE EXECUTABLE_NAME --transport stdio + /// Example: uvx --from mcpforunityserver==9.0.8 mcp-for-unity --transport stdio + /// + private static string BuildClaudeUvxArgs(string uvxPath, string fromUrl, string packageName, bool shouldForceRefresh) + { + // Dev flags + string devFlags = shouldForceRefresh ? "--no-cache --refresh " : string.Empty; + + // Always use --from because package name != executable name + // Example: uvx --from mcpforunityserver==9.0.8 mcp-for-unity + string packageArgs = $"\"{uvxPath}\" {devFlags}--from \"{fromUrl}\" {packageName}"; + + return packageArgs; + } } } diff --git a/MCPForUnity/Editor/Helpers/AssetPathUtility.cs b/MCPForUnity/Editor/Helpers/AssetPathUtility.cs index b65cf21ea..1669bf52c 100644 --- a/MCPForUnity/Editor/Helpers/AssetPathUtility.cs +++ b/MCPForUnity/Editor/Helpers/AssetPathUtility.cs @@ -153,7 +153,7 @@ public static JObject GetPackageJson() /// Checks for EditorPrefs override first (supports git URLs, file:// paths, etc.), /// then falls back to PyPI package reference. /// - /// Package source string for uvx --from argument + /// Package source string for uvx --from argument (guaranteed non-null) public static string GetMcpServerPackageSource() { // Check for override first (supports git URLs, file:// paths, local paths) @@ -182,13 +182,25 @@ public static string GetMcpServerPackageSource() public static string GetMcpServerGitUrl() => GetMcpServerPackageSource(); /// - /// Gets structured uvx command parts for different client configurations + /// Gets structured uvx command parts for different client configurations. /// - /// Tuple containing (uvxPath, fromUrl, packageName) + /// Tuple containing (uvxPath, fromUrl, packageName) - fromUrl is guaranteed non-null + /// Thrown when uvx path cannot be determined public static (string uvxPath, string fromUrl, string packageName) GetUvxCommandParts() { string uvxPath = MCPServiceLocator.Paths.GetUvxPath(); + if (string.IsNullOrEmpty(uvxPath)) + { + throw new InvalidOperationException("Cannot determine uvx path. Please ensure uv is installed and configured."); + } + string fromUrl = GetMcpServerPackageSource(); + // GetMcpServerPackageSource() guarantees non-null, but validate for defense in depth + if (string.IsNullOrEmpty(fromUrl)) + { + throw new InvalidOperationException("Cannot determine MCP server package source. This should never happen - please report this bug."); + } + string packageName = "mcp-for-unity"; return (uvxPath, fromUrl, packageName); diff --git a/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs b/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs index 10f2bacb3..32dae58d0 100644 --- a/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs +++ b/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs @@ -27,6 +27,33 @@ private static void AddDevModeArgs(TomlArray args) args.Add(new TomlString { Value = "--refresh" }); } + /// + /// Adds package arguments to the TOML array. + /// IMPORTANT: We always need --from because the PyPI package name (mcpforunityserver) + /// differs from the executable name (mcp-for-unity). The uvx command format is: + /// uvx --from PACKAGE_SOURCE EXECUTABLE_NAME --transport stdio + /// Example: uvx --from mcpforunityserver==9.0.8 mcp-for-unity --transport stdio + /// + /// The TOML array to add arguments to + /// Package source for --from argument (guaranteed non-null from GetUvxCommandParts) + /// Executable name to run + private static void AddPackageArgs(TomlArray args, string fromUrl, string packageName) + { + if (args == null) return; + + // fromUrl is guaranteed non-null by GetUvxCommandParts(), but validate for defense in depth + if (string.IsNullOrEmpty(fromUrl)) + { + throw new InvalidOperationException("Package source (--from argument) cannot be empty. This should never happen - please report this bug."); + } + + // Always use --from because package name != executable name + args.Add(new TomlString { Value = "--from" }); + args.Add(new TomlString { Value = fromUrl }); + + args.Add(new TomlString { Value = packageName }); + } + public static string BuildCodexServerBlock(string uvPath) { var table = new TomlTable(); @@ -54,12 +81,7 @@ public static string BuildCodexServerBlock(string uvPath) var args = new TomlArray(); AddDevModeArgs(args); - if (!string.IsNullOrEmpty(fromUrl)) - { - args.Add(new TomlString { Value = "--from" }); - args.Add(new TomlString { Value = fromUrl }); - } - args.Add(new TomlString { Value = packageName }); + AddPackageArgs(args, fromUrl, packageName); args.Add(new TomlString { Value = "--transport" }); args.Add(new TomlString { Value = "stdio" }); @@ -203,12 +225,7 @@ private static TomlTable CreateUnityMcpTable(string uvPath) var argsArray = new TomlArray(); AddDevModeArgs(argsArray); - if (!string.IsNullOrEmpty(fromUrl)) - { - argsArray.Add(new TomlString { Value = "--from" }); - argsArray.Add(new TomlString { Value = fromUrl }); - } - argsArray.Add(new TomlString { Value = packageName }); + AddPackageArgs(argsArray, fromUrl, packageName); argsArray.Add(new TomlString { Value = "--transport" }); argsArray.Add(new TomlString { Value = "stdio" }); unityMCP["args"] = argsArray; diff --git a/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs b/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs index dd00d73d0..bc21f878c 100644 --- a/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs +++ b/MCPForUnity/Editor/Helpers/ConfigJsonBuilder.cs @@ -161,7 +161,6 @@ private static IList BuildUvxArgs(string fromUrl, string packageName) // Dev mode: force a fresh install/resolution (avoids stale cached builds while iterating). // `--no-cache` avoids reading from cache; `--refresh` ensures metadata is revalidated. // Note: --reinstall is not supported by uvx and will cause a warning. - // Keep ordering consistent with other uvx builders: dev flags first, then --from , then package name. var args = new List(); // Use central helper that checks both DevModeForceServerRefresh AND local path detection. @@ -170,13 +169,21 @@ private static IList BuildUvxArgs(string fromUrl, string packageName) args.Add("--no-cache"); args.Add("--refresh"); } - if (!string.IsNullOrEmpty(fromUrl)) + + // IMPORTANT: We always need --from because the PyPI package name (mcpforunityserver) + // differs from the executable name (mcp-for-unity). The uvx command format is: + // uvx --from PACKAGE_SOURCE EXECUTABLE_NAME --transport stdio + // Example: uvx --from mcpforunityserver==9.0.8 mcp-for-unity --transport stdio + // fromUrl is guaranteed non-null by GetUvxCommandParts(), but validate for defense in depth + if (string.IsNullOrEmpty(fromUrl)) { - args.Add("--from"); - args.Add(fromUrl); + throw new InvalidOperationException("Package source (--from argument) cannot be empty. This should never happen - please report this bug."); } - args.Add(packageName); + args.Add("--from"); + args.Add(fromUrl); + + args.Add(packageName); args.Add("--transport"); args.Add("stdio");