fix: resolve Claude Code HTTP Remote UV path override not being detected in System Requirements .#550
* fix: resolve UV path override not being detected in System Requirements Fixes #538 The System Requirements panel showed "UV Package Manager: Not Found" even when a valid UV path override was configured in Advanced Settings. Root cause: PlatformDetectorBase.DetectUv() only searched PATH with bare command names ("uvx", "uv") and never consulted PathResolverService which respects the user's override setting. Changes: - Refactor DetectUv() to use PathResolverService.GetUvxPath() which checks override path first, then system PATH, then falls back to "uvx" - Add TryValidateUvExecutable() to verify executables by running --version instead of just checking File.Exists - Prioritize PATH environment variable in EnumerateUvxCandidates() for better compatibility with official uv install scripts - Fix process output read order (ReadToEnd before WaitForExit) to prevent potential deadlocks Co-Authored-By: ChatGLM 4.7 <noreply@zhipuai.com> * fix: improve uv/uvx detection robustness on macOS and Linux - Read both stdout and stderr when validating uv/uvx executables - Respect WaitForExit timeout return value instead of ignoring it - Fix version parsing to handle extra tokens like "(Homebrew 2025-01-01)" - Resolve bare commands ("uv"/"uvx") to absolute paths after validation - Rename FindExecutableInPath to FindUvxExecutableInPath for clarity Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: unify process execution with ExecPath.TryRun and add Windows PATH augmentation Replace direct Process.Start calls with ExecPath.TryRun across all platform detectors. This change: - Fixes potential deadlocks by using async output reading - Adds proper timeout handling with process termination - Removes redundant fallback logic and simplifies version parsing - Adds Windows PATH augmentation with common uv, npm, and Python installation paths Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: improve version parsing to handle both spaces and parentheses The version extraction logic now properly handles outputs like: - "uvx 0.9.18" -> "0.9.18" - "uvx 0.9.18 (hash date)" -> "0.9.18" - "uvx 0.9.18 extra info" -> "0.9.18" Uses Math.Min to find the first occurrence of either space or parenthesis. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: improve platform detectors with absolute path resolution - Add absolute path resolution in TryValidatePython and TryValidateUvWithPath for better UI display - Fix BuildAugmentedPath to avoid PATH duplication - Add comprehensive comments for version parsing logic - Ensure cross-platform consistency across all three detectors - Fix override path validation logic with clear state handling - Fix platform detector path resolution and Python version detection - Use UserProfile consistently in GetClaudeCliPath instead of Personal - All platforms now use protected BuildAugmentedPath method This change improves user experience by displaying full paths in the UI while maintaining robust fallback behavior if path resolution fails. Co-Authored-By: GLM4.7 <noreply@zhipuai.com> * fix: improve error handling in PathResolverService by logging exceptions * Remove .meta files added after fork and update .gitignore * Update .gitignore * save .meta * refactor: unify uv/uvx naming and path detection across platforms - Rename TryValidateUvExecutable -> TryValidateUvxExecutable for consistency - Add cross-platform FindInPath() helper in ExecPath.cs - Remove platform-specific where/which implementations in favor of unified helper - Add Windows-specific DetectUv() override with enhanced uv/uvx detection - Add WinGet shim path support for Windows uvx installation - Update UI labels: "UV Path" -> "UVX Path" - Only show uvx path status when override is configured Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: improve validation light(uvxPathStatus) logic for UVX path overrides and system paths * refactor: streamline UV version validation and unify path detection methods across platform detectors * fix: add type handling for Claude Code client in config JSON builder * fix: correct command from 'uvx' to 'uv' for Python version listing in WindowsPlatformDetector * feat: add uvx path fallback with warning UI - When override path is invalid, automatically fall back to system path - Add HasUvxPathFallback flag to track fallback state - Show yellow warning indicator when using fallback - Display clear messages for invalid override paths - Updated all platform detectors (Windows, macOS, Linux) to support fallback logic * refactor: remove GetDetails method from PlatformDetectorBase * Update ExecPath.cs update Windows Path lookup --------- Co-authored-by: ChatGLM 4.7 <noreply@zhipuai.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Shutong Wu <51266340+Scriptwonder@users.noreply.github.com>main
parent
1ab0fd4ba4
commit
322a3d1846
|
|
@ -42,7 +42,7 @@ namespace MCPForUnity.Editor.Clients
|
|||
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.");
|
||||
throw new InvalidOperationException("uvx not found. Install uv/uvx or set the override in Advanced Settings.");
|
||||
}
|
||||
return uvx;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ using System;
|
|||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using MCPForUnity.Editor.Constants;
|
||||
using MCPForUnity.Editor.Dependencies.Models;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Services;
|
||||
|
||||
namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
|
||||
{
|
||||
|
|
@ -92,37 +94,35 @@ Note: Make sure ~/.local/bin is in your PATH for user-local installations.";
|
|||
|
||||
public override DependencyStatus DetectUv()
|
||||
{
|
||||
var status = new DependencyStatus("uv Package Manager", isRequired: true)
|
||||
// First, honor overrides and cross-platform resolution via the base implementation
|
||||
var status = base.DetectUv();
|
||||
if (status.IsAvailable)
|
||||
{
|
||||
InstallationHint = GetUvInstallUrl()
|
||||
};
|
||||
return status;
|
||||
}
|
||||
|
||||
// If the user configured an override path but fallback was not used, keep the base result
|
||||
// (failure typically means the override path is invalid and no system fallback found)
|
||||
if (MCPServiceLocator.Paths.HasUvxPathOverride && !MCPServiceLocator.Paths.HasUvxPathFallback)
|
||||
{
|
||||
return status;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Try running uv/uvx directly with augmented PATH
|
||||
if (TryValidateUv("uv", out string version, out string fullPath) ||
|
||||
TryValidateUv("uvx", out version, out fullPath))
|
||||
{
|
||||
status.IsAvailable = true;
|
||||
status.Version = version;
|
||||
status.Path = fullPath;
|
||||
status.Details = $"Found uv {version} in PATH";
|
||||
return status;
|
||||
}
|
||||
string augmentedPath = BuildAugmentedPath();
|
||||
|
||||
// Fallback: use which with augmented PATH
|
||||
if (TryFindInPath("uv", out string pathResult) ||
|
||||
TryFindInPath("uvx", out pathResult))
|
||||
{
|
||||
if (TryValidateUv(pathResult, out version, out fullPath))
|
||||
// Try uv first, then uvx, using ExecPath.TryRun for proper timeout handling
|
||||
if (TryValidateUvWithPath("uv", augmentedPath, out string version, out string fullPath) ||
|
||||
TryValidateUvWithPath("uvx", augmentedPath, out version, out fullPath))
|
||||
{
|
||||
status.IsAvailable = true;
|
||||
status.Version = version;
|
||||
status.Path = fullPath;
|
||||
status.Details = $"Found uv {version} in PATH";
|
||||
status.ErrorMessage = null;
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
status.ErrorMessage = "uv not found in PATH";
|
||||
status.Details = "Install uv package manager and ensure it's added to PATH.";
|
||||
|
|
@ -142,45 +142,29 @@ Note: Make sure ~/.local/bin is in your PATH for user-local installations.";
|
|||
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
string augmentedPath = BuildAugmentedPath();
|
||||
|
||||
// First, try to resolve the absolute path for better UI/logging display
|
||||
string commandToRun = pythonPath;
|
||||
if (TryFindInPath(pythonPath, out string resolvedPath))
|
||||
{
|
||||
FileName = pythonPath,
|
||||
Arguments = "--version",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
commandToRun = resolvedPath;
|
||||
}
|
||||
|
||||
// Set PATH to include common locations
|
||||
var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
var pathAdditions = new[]
|
||||
if (!ExecPath.TryRun(commandToRun, "--version", null, out string stdout, out string stderr,
|
||||
5000, augmentedPath))
|
||||
return false;
|
||||
|
||||
// Check stdout first, then stderr (some Python distributions output to stderr)
|
||||
string output = !string.IsNullOrWhiteSpace(stdout) ? stdout.Trim() : stderr.Trim();
|
||||
if (output.StartsWith("Python "))
|
||||
{
|
||||
"/usr/local/bin",
|
||||
"/usr/bin",
|
||||
"/bin",
|
||||
"/snap/bin",
|
||||
Path.Combine(homeDir, ".local", "bin")
|
||||
};
|
||||
version = output.Substring(7);
|
||||
fullPath = commandToRun;
|
||||
|
||||
string currentPath = Environment.GetEnvironmentVariable("PATH") ?? "";
|
||||
psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath;
|
||||
|
||||
using var process = Process.Start(psi);
|
||||
if (process == null) return false;
|
||||
|
||||
string output = process.StandardOutput.ReadToEnd().Trim();
|
||||
process.WaitForExit(5000);
|
||||
|
||||
if (process.ExitCode == 0 && output.StartsWith("Python "))
|
||||
{
|
||||
version = output.Substring(7); // Remove "Python " prefix
|
||||
fullPath = pythonPath;
|
||||
|
||||
// Validate minimum version (Python 4+ or Python 3.10+)
|
||||
if (TryParseVersion(version, out var major, out var minor))
|
||||
{
|
||||
return major > 3 || (major >= 3 && minor >= 10);
|
||||
return major > 3 || (major == 3 && minor >= 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -192,50 +176,13 @@ Note: Make sure ~/.local/bin is in your PATH for user-local installations.";
|
|||
return false;
|
||||
}
|
||||
|
||||
private bool TryValidateUv(string uvPath, out string version, out string fullPath)
|
||||
protected string BuildAugmentedPath()
|
||||
{
|
||||
version = null;
|
||||
fullPath = null;
|
||||
var additions = GetPathAdditions();
|
||||
if (additions.Length == 0) return null;
|
||||
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = uvPath,
|
||||
Arguments = "--version",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
psi.EnvironmentVariables["PATH"] = BuildAugmentedPath();
|
||||
|
||||
using var process = Process.Start(psi);
|
||||
if (process == null) return false;
|
||||
|
||||
string output = process.StandardOutput.ReadToEnd().Trim();
|
||||
process.WaitForExit(5000);
|
||||
|
||||
if (process.ExitCode == 0 && output.StartsWith("uv "))
|
||||
{
|
||||
version = output.Substring(3).Trim();
|
||||
fullPath = uvPath;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore validation errors
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private string BuildAugmentedPath()
|
||||
{
|
||||
string currentPath = Environment.GetEnvironmentVariable("PATH") ?? "";
|
||||
return string.Join(":", GetPathAdditions()) + ":" + currentPath;
|
||||
// Only return the additions - ExecPath.TryRun will prepend to existing PATH
|
||||
return string.Join(Path.PathSeparator, additions);
|
||||
}
|
||||
|
||||
private string[] GetPathAdditions()
|
||||
|
|
@ -251,54 +198,10 @@ Note: Make sure ~/.local/bin is in your PATH for user-local installations.";
|
|||
};
|
||||
}
|
||||
|
||||
private bool TryFindInPath(string executable, out string fullPath)
|
||||
protected override bool TryFindInPath(string executable, out string fullPath)
|
||||
{
|
||||
fullPath = null;
|
||||
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "/usr/bin/which",
|
||||
Arguments = executable,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
// Enhance PATH for Unity's GUI environment
|
||||
var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
var pathAdditions = new[]
|
||||
{
|
||||
"/usr/local/bin",
|
||||
"/usr/bin",
|
||||
"/bin",
|
||||
"/snap/bin",
|
||||
Path.Combine(homeDir, ".local", "bin")
|
||||
};
|
||||
|
||||
string currentPath = Environment.GetEnvironmentVariable("PATH") ?? "";
|
||||
psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath;
|
||||
|
||||
using var process = Process.Start(psi);
|
||||
if (process == null) return false;
|
||||
|
||||
string output = process.StandardOutput.ReadToEnd().Trim();
|
||||
process.WaitForExit(3000);
|
||||
|
||||
if (process.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output))
|
||||
{
|
||||
fullPath = output;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors
|
||||
}
|
||||
|
||||
return false;
|
||||
fullPath = ExecPath.FindInPath(executable, BuildAugmentedPath());
|
||||
return !string.IsNullOrEmpty(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@ using System;
|
|||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using MCPForUnity.Editor.Constants;
|
||||
using MCPForUnity.Editor.Dependencies.Models;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Services;
|
||||
|
||||
namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
|
||||
{
|
||||
|
|
@ -90,37 +92,35 @@ Note: If using Homebrew, make sure /opt/homebrew/bin is in your PATH.";
|
|||
|
||||
public override DependencyStatus DetectUv()
|
||||
{
|
||||
var status = new DependencyStatus("uv Package Manager", isRequired: true)
|
||||
// First, honor overrides and cross-platform resolution via the base implementation
|
||||
var status = base.DetectUv();
|
||||
if (status.IsAvailable)
|
||||
{
|
||||
InstallationHint = GetUvInstallUrl()
|
||||
};
|
||||
return status;
|
||||
}
|
||||
|
||||
// If the user configured an override path but fallback was not used, keep the base result
|
||||
// (failure typically means the override path is invalid and no system fallback found)
|
||||
if (MCPServiceLocator.Paths.HasUvxPathOverride && !MCPServiceLocator.Paths.HasUvxPathFallback)
|
||||
{
|
||||
return status;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Try running uv/uvx directly with augmented PATH
|
||||
if (TryValidateUv("uv", out string version, out string fullPath) ||
|
||||
TryValidateUv("uvx", out version, out fullPath))
|
||||
{
|
||||
status.IsAvailable = true;
|
||||
status.Version = version;
|
||||
status.Path = fullPath;
|
||||
status.Details = $"Found uv {version} in PATH";
|
||||
return status;
|
||||
}
|
||||
string augmentedPath = BuildAugmentedPath();
|
||||
|
||||
// Fallback: use which with augmented PATH
|
||||
if (TryFindInPath("uv", out string pathResult) ||
|
||||
TryFindInPath("uvx", out pathResult))
|
||||
{
|
||||
if (TryValidateUv(pathResult, out version, out fullPath))
|
||||
// Try uv first, then uvx, using ExecPath.TryRun for proper timeout handling
|
||||
if (TryValidateUvWithPath("uv", augmentedPath, out string version, out string fullPath) ||
|
||||
TryValidateUvWithPath("uvx", augmentedPath, out version, out fullPath))
|
||||
{
|
||||
status.IsAvailable = true;
|
||||
status.Version = version;
|
||||
status.Path = fullPath;
|
||||
status.Details = $"Found uv {version} in PATH";
|
||||
status.ErrorMessage = null;
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
status.ErrorMessage = "uv not found in PATH";
|
||||
status.Details = "Install uv package manager and ensure it's added to PATH.";
|
||||
|
|
@ -140,44 +140,29 @@ Note: If using Homebrew, make sure /opt/homebrew/bin is in your PATH.";
|
|||
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
string augmentedPath = BuildAugmentedPath();
|
||||
|
||||
// First, try to resolve the absolute path for better UI/logging display
|
||||
string commandToRun = pythonPath;
|
||||
if (TryFindInPath(pythonPath, out string resolvedPath))
|
||||
{
|
||||
FileName = pythonPath,
|
||||
Arguments = "--version",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
commandToRun = resolvedPath;
|
||||
}
|
||||
|
||||
// Set PATH to include common locations
|
||||
var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
var pathAdditions = new[]
|
||||
if (!ExecPath.TryRun(commandToRun, "--version", null, out string stdout, out string stderr,
|
||||
5000, augmentedPath))
|
||||
return false;
|
||||
|
||||
// Check stdout first, then stderr (some Python distributions output to stderr)
|
||||
string output = !string.IsNullOrWhiteSpace(stdout) ? stdout.Trim() : stderr.Trim();
|
||||
if (output.StartsWith("Python "))
|
||||
{
|
||||
"/opt/homebrew/bin",
|
||||
"/usr/local/bin",
|
||||
"/usr/bin",
|
||||
Path.Combine(homeDir, ".local", "bin")
|
||||
};
|
||||
version = output.Substring(7);
|
||||
fullPath = commandToRun;
|
||||
|
||||
string currentPath = Environment.GetEnvironmentVariable("PATH") ?? "";
|
||||
psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath;
|
||||
|
||||
using var process = Process.Start(psi);
|
||||
if (process == null) return false;
|
||||
|
||||
string output = process.StandardOutput.ReadToEnd().Trim();
|
||||
process.WaitForExit(5000);
|
||||
|
||||
if (process.ExitCode == 0 && output.StartsWith("Python "))
|
||||
{
|
||||
version = output.Substring(7); // Remove "Python " prefix
|
||||
fullPath = pythonPath;
|
||||
|
||||
// Validate minimum version (Python 4+ or Python 3.10+)
|
||||
if (TryParseVersion(version, out var major, out var minor))
|
||||
{
|
||||
return major > 3 || (major >= 3 && minor >= 10);
|
||||
return major > 3 || (major == 3 && minor >= 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -189,52 +174,13 @@ Note: If using Homebrew, make sure /opt/homebrew/bin is in your PATH.";
|
|||
return false;
|
||||
}
|
||||
|
||||
private bool TryValidateUv(string uvPath, out string version, out string fullPath)
|
||||
protected string BuildAugmentedPath()
|
||||
{
|
||||
version = null;
|
||||
fullPath = null;
|
||||
var additions = GetPathAdditions();
|
||||
if (additions.Length == 0) return null;
|
||||
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = uvPath,
|
||||
Arguments = "--version",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
var augmentedPath = BuildAugmentedPath();
|
||||
psi.EnvironmentVariables["PATH"] = augmentedPath;
|
||||
|
||||
using var process = Process.Start(psi);
|
||||
if (process == null) return false;
|
||||
|
||||
string output = process.StandardOutput.ReadToEnd().Trim();
|
||||
process.WaitForExit(5000);
|
||||
|
||||
if (process.ExitCode == 0 && output.StartsWith("uv "))
|
||||
{
|
||||
version = output.Substring(3).Trim();
|
||||
fullPath = uvPath;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore validation errors
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private string BuildAugmentedPath()
|
||||
{
|
||||
var pathAdditions = GetPathAdditions();
|
||||
string currentPath = Environment.GetEnvironmentVariable("PATH") ?? "";
|
||||
return string.Join(":", pathAdditions) + ":" + currentPath;
|
||||
// Only return the additions - ExecPath.TryRun will prepend to existing PATH
|
||||
return string.Join(Path.PathSeparator, additions);
|
||||
}
|
||||
|
||||
private string[] GetPathAdditions()
|
||||
|
|
@ -250,54 +196,10 @@ Note: If using Homebrew, make sure /opt/homebrew/bin is in your PATH.";
|
|||
};
|
||||
}
|
||||
|
||||
private bool TryFindInPath(string executable, out string fullPath)
|
||||
protected override bool TryFindInPath(string executable, out string fullPath)
|
||||
{
|
||||
fullPath = null;
|
||||
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "/usr/bin/which",
|
||||
Arguments = executable,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
// Enhance PATH for Unity's GUI environment
|
||||
var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
var pathAdditions = new[]
|
||||
{
|
||||
"/opt/homebrew/bin",
|
||||
"/usr/local/bin",
|
||||
"/usr/bin",
|
||||
"/bin",
|
||||
Path.Combine(homeDir, ".local", "bin")
|
||||
};
|
||||
|
||||
string currentPath = Environment.GetEnvironmentVariable("PATH") ?? "";
|
||||
psi.EnvironmentVariables["PATH"] = string.Join(":", pathAdditions) + ":" + currentPath;
|
||||
|
||||
using var process = Process.Start(psi);
|
||||
if (process == null) return false;
|
||||
|
||||
string output = process.StandardOutput.ReadToEnd().Trim();
|
||||
process.WaitForExit(3000);
|
||||
|
||||
if (process.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output))
|
||||
{
|
||||
fullPath = output;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors
|
||||
}
|
||||
|
||||
return false;
|
||||
fullPath = ExecPath.FindInPath(executable, BuildAugmentedPath());
|
||||
return !string.IsNullOrEmpty(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using MCPForUnity.Editor.Dependencies.Models;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Services;
|
||||
|
||||
namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
|
||||
{
|
||||
|
|
@ -26,70 +27,42 @@ namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
|
|||
|
||||
try
|
||||
{
|
||||
// Try to find uv/uvx in PATH
|
||||
if (TryFindUvInPath(out string uvPath, out string version))
|
||||
// Get uv path from PathResolverService (respects override)
|
||||
string uvxPath = MCPServiceLocator.Paths.GetUvxPath();
|
||||
|
||||
// Verify uv executable and get version
|
||||
if (MCPServiceLocator.Paths.TryValidateUvxExecutable(uvxPath, out string version))
|
||||
{
|
||||
status.IsAvailable = true;
|
||||
status.Version = version;
|
||||
status.Path = uvPath;
|
||||
status.Details = $"Found uv {version} in PATH";
|
||||
status.Path = uvxPath;
|
||||
|
||||
// Check if we used fallback from override to system path
|
||||
if (MCPServiceLocator.Paths.HasUvxPathFallback)
|
||||
{
|
||||
status.Details = $"Found uv {version} (fallback to system path)";
|
||||
status.ErrorMessage = "Override path not found, using system path";
|
||||
}
|
||||
else
|
||||
{
|
||||
status.Details = MCPServiceLocator.Paths.HasUvxPathOverride
|
||||
? $"Found uv {version} (override path)"
|
||||
: $"Found uv {version} in system path";
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
status.ErrorMessage = "uv not found in PATH";
|
||||
status.Details = "Install uv package manager and ensure it's added to PATH.";
|
||||
status.ErrorMessage = "uvx not found";
|
||||
status.Details = "Install uv package manager or configure path override in Advanced Settings.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
status.ErrorMessage = $"Error detecting uv: {ex.Message}";
|
||||
status.ErrorMessage = $"Error detecting uvx: {ex.Message}";
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
protected bool TryFindUvInPath(out string uvPath, out string version)
|
||||
{
|
||||
uvPath = null;
|
||||
version = null;
|
||||
|
||||
// Try common uv command names
|
||||
var commands = new[] { "uvx", "uv" };
|
||||
|
||||
foreach (var cmd in commands)
|
||||
{
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = cmd,
|
||||
Arguments = "--version",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
using var process = Process.Start(psi);
|
||||
if (process == null) continue;
|
||||
|
||||
string output = process.StandardOutput.ReadToEnd().Trim();
|
||||
process.WaitForExit(5000);
|
||||
|
||||
if (process.ExitCode == 0 && output.StartsWith("uv "))
|
||||
{
|
||||
version = output.Substring(3).Trim();
|
||||
uvPath = cmd;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Try next command
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected bool TryParseVersion(string version, out int major, out int minor)
|
||||
{
|
||||
|
|
@ -111,5 +84,54 @@ namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
|
|||
|
||||
return false;
|
||||
}
|
||||
// In PlatformDetectorBase.cs
|
||||
protected bool TryValidateUvWithPath(string command, string augmentedPath, out string version, out string fullPath)
|
||||
{
|
||||
version = null;
|
||||
fullPath = null;
|
||||
|
||||
try
|
||||
{
|
||||
string commandToRun = command;
|
||||
if (TryFindInPath(command, out string resolvedPath))
|
||||
{
|
||||
commandToRun = resolvedPath;
|
||||
}
|
||||
|
||||
if (!ExecPath.TryRun(commandToRun, "--version", null, out string stdout, out string stderr,
|
||||
5000, augmentedPath))
|
||||
return false;
|
||||
|
||||
string output = string.IsNullOrWhiteSpace(stdout) ? stderr.Trim() : stdout.Trim();
|
||||
|
||||
if (output.StartsWith("uvx ") || output.StartsWith("uv "))
|
||||
{
|
||||
int spaceIndex = output.IndexOf(' ');
|
||||
if (spaceIndex >= 0)
|
||||
{
|
||||
var remainder = output.Substring(spaceIndex + 1).Trim();
|
||||
int nextSpace = remainder.IndexOf(' ');
|
||||
int parenIndex = remainder.IndexOf('(');
|
||||
int endIndex = Math.Min(
|
||||
nextSpace >= 0 ? nextSpace : int.MaxValue,
|
||||
parenIndex >= 0 ? parenIndex : int.MaxValue
|
||||
);
|
||||
version = endIndex < int.MaxValue ? remainder.Substring(0, endIndex).Trim() : remainder;
|
||||
fullPath = commandToRun;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore validation errors
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Add abstract method for subclasses to implement
|
||||
protected abstract bool TryFindInPath(string executable, out string fullPath);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using MCPForUnity.Editor.Constants;
|
||||
using MCPForUnity.Editor.Dependencies.Models;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Services;
|
||||
|
||||
namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
|
||||
{
|
||||
|
|
@ -96,6 +100,58 @@ namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
|
|||
3. MCP Server: Will be installed automatically by MCP for Unity Bridge";
|
||||
}
|
||||
|
||||
public override DependencyStatus DetectUv()
|
||||
{
|
||||
// First, honor overrides and cross-platform resolution via the base implementation
|
||||
var status = base.DetectUv();
|
||||
if (status.IsAvailable)
|
||||
{
|
||||
return status;
|
||||
}
|
||||
|
||||
// If the user configured an override path but fallback was not used, keep the base result
|
||||
// (failure typically means the override path is invalid and no system fallback found)
|
||||
if (MCPServiceLocator.Paths.HasUvxPathOverride && !MCPServiceLocator.Paths.HasUvxPathFallback)
|
||||
{
|
||||
return status;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string augmentedPath = BuildAugmentedPath();
|
||||
|
||||
// try to find uv
|
||||
if (TryValidateUvWithPath("uv.exe", augmentedPath, out string uvVersion, out string uvPath))
|
||||
{
|
||||
status.IsAvailable = true;
|
||||
status.Version = uvVersion;
|
||||
status.Path = uvPath;
|
||||
status.Details = $"Found uv {uvVersion} at {uvPath}";
|
||||
return status;
|
||||
}
|
||||
|
||||
// try to find uvx
|
||||
if (TryValidateUvWithPath("uvx.exe", augmentedPath, out string uvxVersion, out string uvxPath))
|
||||
{
|
||||
status.IsAvailable = true;
|
||||
status.Version = uvxVersion;
|
||||
status.Path = uvxPath;
|
||||
status.Details = $"Found uvx {uvxVersion} at {uvxPath} (fallback)";
|
||||
return status;
|
||||
}
|
||||
|
||||
status.ErrorMessage = "uv not found in PATH";
|
||||
status.Details = "Install uv package manager and ensure it's added to PATH.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
status.ErrorMessage = $"Error detecting uv: {ex.Message}";
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
|
||||
private bool TryFindPythonViaUv(out string version, out string fullPath)
|
||||
{
|
||||
version = null;
|
||||
|
|
@ -103,33 +159,16 @@ namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
|
|||
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "uv", // Assume uv is in path or user can't use this fallback
|
||||
Arguments = "python list",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
string augmentedPath = BuildAugmentedPath();
|
||||
// Try to list installed python versions via uvx
|
||||
if (!ExecPath.TryRun("uv", "python list", null, out string stdout, out string stderr, 5000, augmentedPath))
|
||||
return false;
|
||||
|
||||
using var process = Process.Start(psi);
|
||||
if (process == null) return false;
|
||||
|
||||
string output = process.StandardOutput.ReadToEnd();
|
||||
process.WaitForExit(5000);
|
||||
|
||||
if (process.ExitCode == 0 && !string.IsNullOrEmpty(output))
|
||||
{
|
||||
var lines = output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var lines = stdout.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var line in lines)
|
||||
{
|
||||
// Look for installed python paths
|
||||
// Format is typically: <version> <path>
|
||||
// Skip lines with "<download available>"
|
||||
if (line.Contains("<download available>")) continue;
|
||||
|
||||
// The path is typically the last part of the line
|
||||
var parts = line.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length >= 2)
|
||||
{
|
||||
|
|
@ -145,7 +184,6 @@ namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors if uv is not installed or fails
|
||||
|
|
@ -161,31 +199,29 @@ namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
|
|||
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
string augmentedPath = BuildAugmentedPath();
|
||||
|
||||
// First, try to resolve the absolute path for better UI/logging display
|
||||
string commandToRun = pythonPath;
|
||||
if (TryFindInPath(pythonPath, out string resolvedPath))
|
||||
{
|
||||
FileName = pythonPath,
|
||||
Arguments = "--version",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
commandToRun = resolvedPath;
|
||||
}
|
||||
|
||||
using var process = Process.Start(psi);
|
||||
if (process == null) return false;
|
||||
// Run 'python --version' to get the version
|
||||
if (!ExecPath.TryRun(commandToRun, "--version", null, out string stdout, out string stderr, 5000, augmentedPath))
|
||||
return false;
|
||||
|
||||
string output = process.StandardOutput.ReadToEnd().Trim();
|
||||
process.WaitForExit(5000);
|
||||
|
||||
if (process.ExitCode == 0 && output.StartsWith("Python "))
|
||||
// Check stdout first, then stderr (some Python distributions output to stderr)
|
||||
string output = !string.IsNullOrWhiteSpace(stdout) ? stdout.Trim() : stderr.Trim();
|
||||
if (output.StartsWith("Python "))
|
||||
{
|
||||
version = output.Substring(7); // Remove "Python " prefix
|
||||
fullPath = pythonPath;
|
||||
version = output.Substring(7);
|
||||
fullPath = commandToRun;
|
||||
|
||||
// Validate minimum version (Python 4+ or Python 3.10+)
|
||||
if (TryParseVersion(version, out var major, out var minor))
|
||||
{
|
||||
return major > 3 || (major >= 3 && minor >= 10);
|
||||
return major > 3 || (major == 3 && minor >= 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -197,45 +233,65 @@ namespace MCPForUnity.Editor.Dependencies.PlatformDetectors
|
|||
return false;
|
||||
}
|
||||
|
||||
private bool TryFindInPath(string executable, out string fullPath)
|
||||
protected override bool TryFindInPath(string executable, out string fullPath)
|
||||
{
|
||||
fullPath = null;
|
||||
fullPath = ExecPath.FindInPath(executable, BuildAugmentedPath());
|
||||
return !string.IsNullOrEmpty(fullPath);
|
||||
}
|
||||
|
||||
protected string BuildAugmentedPath()
|
||||
{
|
||||
var additions = GetPathAdditions();
|
||||
if (additions.Length == 0) return null;
|
||||
|
||||
// Only return the additions - ExecPath.TryRun will prepend to existing PATH
|
||||
return string.Join(Path.PathSeparator, additions);
|
||||
}
|
||||
|
||||
private string[] GetPathAdditions()
|
||||
{
|
||||
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
|
||||
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
|
||||
var additions = new List<string>();
|
||||
|
||||
// uv common installation paths
|
||||
if (!string.IsNullOrEmpty(localAppData))
|
||||
additions.Add(Path.Combine(localAppData, "Programs", "uv"));
|
||||
if (!string.IsNullOrEmpty(programFiles))
|
||||
additions.Add(Path.Combine(programFiles, "uv"));
|
||||
|
||||
// npm global paths
|
||||
if (!string.IsNullOrEmpty(appData))
|
||||
additions.Add(Path.Combine(appData, "npm"));
|
||||
if (!string.IsNullOrEmpty(localAppData))
|
||||
additions.Add(Path.Combine(localAppData, "npm"));
|
||||
|
||||
// Python common paths
|
||||
if (!string.IsNullOrEmpty(localAppData))
|
||||
additions.Add(Path.Combine(localAppData, "Programs", "Python"));
|
||||
// Instead of hardcoded versions, enumerate existing directories
|
||||
if (!string.IsNullOrEmpty(programFiles))
|
||||
{
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
var pythonDirs = Directory.GetDirectories(programFiles, "Python3*")
|
||||
.OrderByDescending(d => d); // Newest first
|
||||
foreach (var dir in pythonDirs)
|
||||
{
|
||||
FileName = "where",
|
||||
Arguments = executable,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
using var process = Process.Start(psi);
|
||||
if (process == null) return false;
|
||||
|
||||
string output = process.StandardOutput.ReadToEnd().Trim();
|
||||
process.WaitForExit(3000);
|
||||
|
||||
if (process.ExitCode == 0 && !string.IsNullOrEmpty(output))
|
||||
{
|
||||
// Take the first result
|
||||
var lines = output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (lines.Length > 0)
|
||||
{
|
||||
fullPath = lines[0].Trim();
|
||||
return File.Exists(fullPath);
|
||||
additions.Add(dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors
|
||||
catch { /* Ignore if directory doesn't exist */ }
|
||||
}
|
||||
|
||||
return false;
|
||||
// User scripts
|
||||
if (!string.IsNullOrEmpty(homeDir))
|
||||
additions.Add(Path.Combine(homeDir, ".local", "bin"));
|
||||
|
||||
return additions.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,6 +77,11 @@ namespace MCPForUnity.Editor.Helpers
|
|||
{
|
||||
unity["type"] = "http";
|
||||
}
|
||||
// Also add type for Claude Code (uses mcpServers layout but needs type field)
|
||||
else if (client?.name == "Claude Code")
|
||||
{
|
||||
unity["type"] = "http";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -110,8 +115,8 @@ namespace MCPForUnity.Editor.Helpers
|
|||
}
|
||||
}
|
||||
|
||||
// Remove type for non-VSCode clients
|
||||
if (!isVSCode && unity["type"] != null)
|
||||
// Remove type for non-VSCode clients (except Claude Code which needs it)
|
||||
if (!isVSCode && client?.name != "Claude Code" && unity["type"] != null)
|
||||
{
|
||||
unity.Remove("type");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ namespace MCPForUnity.Editor.Helpers
|
|||
Path.Combine(localAppData, "npm", "claude.ps1"),
|
||||
};
|
||||
foreach (string c in candidates) { if (File.Exists(c)) return c; }
|
||||
string fromWhere = Where("claude.exe") ?? Where("claude.cmd") ?? Where("claude.ps1") ?? Where("claude");
|
||||
string fromWhere = FindInPathWindows("claude.exe") ?? FindInPathWindows("claude.cmd") ?? FindInPathWindows("claude.ps1") ?? FindInPathWindows("claude");
|
||||
if (!string.IsNullOrEmpty(fromWhere)) return fromWhere;
|
||||
#endif
|
||||
return null;
|
||||
|
|
@ -197,9 +197,9 @@ namespace MCPForUnity.Editor.Helpers
|
|||
|
||||
using var process = new Process { StartInfo = psi, EnableRaisingEvents = false };
|
||||
|
||||
var so = new StringBuilder();
|
||||
var sb = new StringBuilder();
|
||||
var se = new StringBuilder();
|
||||
process.OutputDataReceived += (_, e) => { if (e.Data != null) so.AppendLine(e.Data); };
|
||||
process.OutputDataReceived += (_, e) => { if (e.Data != null) sb.AppendLine(e.Data); };
|
||||
process.ErrorDataReceived += (_, e) => { if (e.Data != null) se.AppendLine(e.Data); };
|
||||
|
||||
if (!process.Start()) return false;
|
||||
|
|
@ -216,7 +216,7 @@ namespace MCPForUnity.Editor.Helpers
|
|||
// Ensure async buffers are flushed
|
||||
process.WaitForExit();
|
||||
|
||||
stdout = so.ToString();
|
||||
stdout = sb.ToString();
|
||||
stderr = se.ToString();
|
||||
return process.ExitCode == 0;
|
||||
}
|
||||
|
|
@ -226,6 +226,21 @@ namespace MCPForUnity.Editor.Helpers
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cross-platform path lookup. Uses 'where' on Windows, 'which' on macOS/Linux.
|
||||
/// Returns the full path if found, null otherwise.
|
||||
/// </summary>
|
||||
internal static string FindInPath(string executable, string extraPathPrepend = null)
|
||||
{
|
||||
#if UNITY_EDITOR_WIN
|
||||
return FindInPathWindows(executable, extraPathPrepend);
|
||||
#elif UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
|
||||
return Which(executable, extraPathPrepend ?? string.Empty);
|
||||
#else
|
||||
return null;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR_OSX || UNITY_EDITOR_LINUX
|
||||
private static string Which(string exe, string prependPath)
|
||||
{
|
||||
|
|
@ -239,9 +254,22 @@ namespace MCPForUnity.Editor.Helpers
|
|||
};
|
||||
string path = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
|
||||
psi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(path) ? prependPath : (prependPath + Path.PathSeparator + path);
|
||||
|
||||
using var p = Process.Start(psi);
|
||||
string output = p?.StandardOutput.ReadToEnd().Trim();
|
||||
p?.WaitForExit(1500);
|
||||
if (p == null) return null;
|
||||
|
||||
var so = new StringBuilder();
|
||||
p.OutputDataReceived += (_, e) => { if (e.Data != null) so.AppendLine(e.Data); };
|
||||
p.BeginOutputReadLine();
|
||||
|
||||
if (!p.WaitForExit(1500))
|
||||
{
|
||||
try { p.Kill(); } catch { }
|
||||
return null;
|
||||
}
|
||||
|
||||
p.WaitForExit();
|
||||
string output = so.ToString().Trim();
|
||||
return (!string.IsNullOrEmpty(output) && File.Exists(output)) ? output : null;
|
||||
}
|
||||
catch { return null; }
|
||||
|
|
@ -249,21 +277,44 @@ namespace MCPForUnity.Editor.Helpers
|
|||
#endif
|
||||
|
||||
#if UNITY_EDITOR_WIN
|
||||
private static string Where(string exe)
|
||||
private static string FindInPathWindows(string exe, string extraPathPrepend = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
string currentPath = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
|
||||
string effectivePath = string.IsNullOrEmpty(extraPathPrepend)
|
||||
? currentPath
|
||||
: (string.IsNullOrEmpty(currentPath) ? extraPathPrepend : extraPathPrepend + Path.PathSeparator + currentPath);
|
||||
|
||||
var psi = new ProcessStartInfo("where", exe)
|
||||
{
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true,
|
||||
};
|
||||
if (!string.IsNullOrEmpty(effectivePath))
|
||||
{
|
||||
psi.EnvironmentVariables["PATH"] = effectivePath;
|
||||
}
|
||||
|
||||
using var p = Process.Start(psi);
|
||||
string first = p?.StandardOutput.ReadToEnd()
|
||||
if (p == null) return null;
|
||||
|
||||
var so = new StringBuilder();
|
||||
p.OutputDataReceived += (_, e) => { if (e.Data != null) so.AppendLine(e.Data); };
|
||||
p.BeginOutputReadLine();
|
||||
|
||||
if (!p.WaitForExit(1500))
|
||||
{
|
||||
try { p.Kill(); } catch { }
|
||||
return null;
|
||||
}
|
||||
|
||||
p.WaitForExit();
|
||||
string first = so.ToString()
|
||||
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.FirstOrDefault();
|
||||
p?.WaitForExit(1500);
|
||||
return (!string.IsNullOrEmpty(first) && File.Exists(first)) ? first : null;
|
||||
}
|
||||
catch { return null; }
|
||||
|
|
|
|||
|
|
@ -60,5 +60,18 @@ namespace MCPForUnity.Editor.Services
|
|||
/// Gets whether a Claude CLI path override is active
|
||||
/// </summary>
|
||||
bool HasClaudeCliPathOverride { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the uvx path used a fallback from override to system path
|
||||
/// </summary>
|
||||
bool HasUvxPathFallback { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Validates the provided uv executable by running "--version" and parsing the output.
|
||||
/// </summary>
|
||||
/// <param name="uvPath">Absolute or relative path to the uv/uvx executable.</param>
|
||||
/// <param name="version">Parsed version string if successful.</param>
|
||||
/// <returns>True when the executable runs and returns a uv version string.</returns>
|
||||
bool TryValidateUvxExecutable(string uvPath, out string version);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,46 +16,98 @@ namespace MCPForUnity.Editor.Services
|
|||
/// </summary>
|
||||
public class PathResolverService : IPathResolverService
|
||||
{
|
||||
private bool _hasUvxPathFallback;
|
||||
|
||||
public bool HasUvxPathOverride => !string.IsNullOrEmpty(EditorPrefs.GetString(EditorPrefKeys.UvxPathOverride, null));
|
||||
public bool HasClaudeCliPathOverride => !string.IsNullOrEmpty(EditorPrefs.GetString(EditorPrefKeys.ClaudeCliPathOverride, null));
|
||||
public bool HasUvxPathFallback => _hasUvxPathFallback;
|
||||
|
||||
public string GetUvxPath()
|
||||
{
|
||||
try
|
||||
// Reset fallback flag at the start of each resolution
|
||||
_hasUvxPathFallback = false;
|
||||
|
||||
// Check override first - only validate if explicitly set
|
||||
if (HasUvxPathOverride)
|
||||
{
|
||||
string overridePath = EditorPrefs.GetString(EditorPrefKeys.UvxPathOverride, string.Empty);
|
||||
if (!string.IsNullOrEmpty(overridePath))
|
||||
// Validate the override - if invalid, fall back to system discovery
|
||||
if (TryValidateUvxExecutable(overridePath, out string version))
|
||||
{
|
||||
return overridePath;
|
||||
}
|
||||
}
|
||||
catch
|
||||
// Override is set but invalid - fall back to system discovery
|
||||
string fallbackPath = ResolveUvxFromSystem();
|
||||
if (!string.IsNullOrEmpty(fallbackPath))
|
||||
{
|
||||
// ignore EditorPrefs read errors and fall back to default command
|
||||
McpLog.Debug("No uvx path override found, falling back to default command");
|
||||
_hasUvxPathFallback = true;
|
||||
return fallbackPath;
|
||||
}
|
||||
// Return null to indicate override is invalid and no system fallback found
|
||||
return null;
|
||||
}
|
||||
|
||||
// No override set - try discovery (uvx first, then uv)
|
||||
string discovered = ResolveUvxFromSystem();
|
||||
if (!string.IsNullOrEmpty(discovered))
|
||||
{
|
||||
return discovered;
|
||||
}
|
||||
|
||||
// Fallback to bare command
|
||||
return "uvx";
|
||||
}
|
||||
|
||||
public string GetClaudeCliPath()
|
||||
/// <summary>
|
||||
/// Resolves uv/uvx from system by trying both commands.
|
||||
/// Returns the full path if found, null otherwise.
|
||||
/// </summary>
|
||||
private static string ResolveUvxFromSystem()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Try uvx first, then uv
|
||||
string[] commandNames = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
||||
? new[] { "uvx.exe", "uv.exe" }
|
||||
: new[] { "uvx", "uv" };
|
||||
|
||||
foreach (string commandName in commandNames)
|
||||
{
|
||||
foreach (string candidate in EnumerateCommandCandidates(commandName))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(candidate) && File.Exists(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Debug($"PathResolver error: {ex.Message}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public string GetClaudeCliPath()
|
||||
{
|
||||
// Check override first - only validate if explicitly set
|
||||
if (HasClaudeCliPathOverride)
|
||||
{
|
||||
string overridePath = EditorPrefs.GetString(EditorPrefKeys.ClaudeCliPathOverride, string.Empty);
|
||||
if (!string.IsNullOrEmpty(overridePath) && File.Exists(overridePath))
|
||||
// Validate the override - if invalid, don't fall back to discovery
|
||||
if (File.Exists(overridePath))
|
||||
{
|
||||
return overridePath;
|
||||
}
|
||||
// Override is set but invalid - return null (no fallback)
|
||||
return null;
|
||||
}
|
||||
catch { /* ignore */ }
|
||||
|
||||
// No override - use platform-specific discovery
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
string[] candidates = new[]
|
||||
|
|
@ -76,7 +128,7 @@ namespace MCPForUnity.Editor.Services
|
|||
{
|
||||
"/opt/homebrew/bin/claude",
|
||||
"/usr/local/bin/claude",
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), ".local", "bin", "claude")
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "bin", "claude")
|
||||
};
|
||||
|
||||
foreach (var c in candidates)
|
||||
|
|
@ -90,7 +142,7 @@ namespace MCPForUnity.Editor.Services
|
|||
{
|
||||
"/usr/bin/claude",
|
||||
"/usr/local/bin/claude",
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), ".local", "bin", "claude")
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "bin", "claude")
|
||||
};
|
||||
|
||||
foreach (var c in candidates)
|
||||
|
|
@ -104,25 +156,13 @@ namespace MCPForUnity.Editor.Services
|
|||
|
||||
public bool IsPythonDetected()
|
||||
{
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "python.exe" : "python3",
|
||||
Arguments = "--version",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
using var p = Process.Start(psi);
|
||||
p.WaitForExit(2000);
|
||||
return p.ExitCode == 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return ExecPath.TryRun(
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "python.exe" : "python3",
|
||||
"--version",
|
||||
null,
|
||||
out _,
|
||||
out _,
|
||||
2000);
|
||||
}
|
||||
|
||||
public bool IsClaudeCliDetected()
|
||||
|
|
@ -130,81 +170,6 @@ namespace MCPForUnity.Editor.Services
|
|||
return !string.IsNullOrEmpty(GetClaudeCliPath());
|
||||
}
|
||||
|
||||
private static string ResolveUvxFromSystem()
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (string candidate in EnumerateUvxCandidates())
|
||||
{
|
||||
if (!string.IsNullOrEmpty(candidate) && File.Exists(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// fall back to bare command
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateUvxCandidates()
|
||||
{
|
||||
string exeName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "uvx.exe" : "uvx";
|
||||
|
||||
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
if (!string.IsNullOrEmpty(home))
|
||||
{
|
||||
yield return Path.Combine(home, ".local", "bin", exeName);
|
||||
yield return Path.Combine(home, ".cargo", "bin", exeName);
|
||||
}
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
yield return "/opt/homebrew/bin/" + exeName;
|
||||
yield return "/usr/local/bin/" + exeName;
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
yield return "/usr/local/bin/" + exeName;
|
||||
yield return "/usr/bin/" + exeName;
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
|
||||
|
||||
if (!string.IsNullOrEmpty(localAppData))
|
||||
{
|
||||
yield return Path.Combine(localAppData, "Programs", "uv", exeName);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(programFiles))
|
||||
{
|
||||
yield return Path.Combine(programFiles, "uv", exeName);
|
||||
}
|
||||
}
|
||||
|
||||
string pathEnv = Environment.GetEnvironmentVariable("PATH");
|
||||
if (!string.IsNullOrEmpty(pathEnv))
|
||||
{
|
||||
foreach (string rawDir in pathEnv.Split(Path.PathSeparator))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rawDir)) continue;
|
||||
string dir = rawDir.Trim();
|
||||
yield return Path.Combine(dir, exeName);
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
// Some PATH entries may already contain the file without extension
|
||||
yield return Path.Combine(dir, "uvx");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetUvxPathOverride(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
|
|
@ -246,5 +211,148 @@ namespace MCPForUnity.Editor.Services
|
|||
{
|
||||
EditorPrefs.DeleteKey(EditorPrefKeys.ClaudeCliPathOverride);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the provided uv executable by running "--version" and parsing the output.
|
||||
/// </summary>
|
||||
/// <param name="uvxPath">Absolute or relative path to the uv/uvx executable.</param>
|
||||
/// <param name="version">Parsed version string if successful.</param>
|
||||
/// <returns>True when the executable runs and returns a uvx version string.</returns>
|
||||
public bool TryValidateUvxExecutable(string uvxPath, out string version)
|
||||
{
|
||||
version = null;
|
||||
|
||||
if (string.IsNullOrEmpty(uvxPath))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
// Check if the path is just a command name (no directory separator)
|
||||
bool isBareCommand = !uvxPath.Contains('/') && !uvxPath.Contains('\\');
|
||||
|
||||
if (isBareCommand)
|
||||
{
|
||||
// For bare commands like "uvx" or "uv", use EnumerateCommandCandidates to find full path first
|
||||
string fullPath = FindUvxExecutableInPath(uvxPath);
|
||||
if (string.IsNullOrEmpty(fullPath))
|
||||
return false;
|
||||
uvxPath = fullPath;
|
||||
}
|
||||
|
||||
// Use ExecPath.TryRun which properly handles async output reading and timeouts
|
||||
if (!ExecPath.TryRun(uvxPath, "--version", null, out string stdout, out string stderr, 5000))
|
||||
return false;
|
||||
|
||||
// Check stdout first, then stderr (some tools output to stderr)
|
||||
string versionOutput = !string.IsNullOrWhiteSpace(stdout) ? stdout.Trim() : stderr.Trim();
|
||||
|
||||
// uv/uvx outputs "uv x.y.z" or "uvx x.y.z", extract version number
|
||||
if (versionOutput.StartsWith("uvx ") || versionOutput.StartsWith("uv "))
|
||||
{
|
||||
// Extract version: "uv 0.9.18 (hash date)" -> "0.9.18"
|
||||
int spaceIndex = versionOutput.IndexOf(' ');
|
||||
if (spaceIndex >= 0)
|
||||
{
|
||||
string afterCommand = versionOutput.Substring(spaceIndex + 1).Trim();
|
||||
// Version is up to the first space or parenthesis
|
||||
int nextSpace = afterCommand.IndexOf(' ');
|
||||
int parenIndex = afterCommand.IndexOf('(');
|
||||
int endIndex = Math.Min(
|
||||
nextSpace >= 0 ? nextSpace : int.MaxValue,
|
||||
parenIndex >= 0 ? parenIndex : int.MaxValue
|
||||
);
|
||||
version = endIndex < int.MaxValue ? afterCommand.Substring(0, endIndex).Trim() : afterCommand;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore validation errors
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private string FindUvxExecutableInPath(string commandName)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Generic search for any command in PATH and common locations
|
||||
foreach (string candidate in EnumerateCommandCandidates(commandName))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(candidate) && File.Exists(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates candidate paths for a generic command name.
|
||||
/// Searches PATH and common locations.
|
||||
/// </summary>
|
||||
private static IEnumerable<string> EnumerateCommandCandidates(string commandName)
|
||||
{
|
||||
string exeName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !commandName.EndsWith(".exe")
|
||||
? commandName + ".exe"
|
||||
: commandName;
|
||||
|
||||
// Search PATH first
|
||||
string pathEnv = Environment.GetEnvironmentVariable("PATH");
|
||||
if (!string.IsNullOrEmpty(pathEnv))
|
||||
{
|
||||
foreach (string rawDir in pathEnv.Split(Path.PathSeparator))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rawDir)) continue;
|
||||
string dir = rawDir.Trim();
|
||||
yield return Path.Combine(dir, exeName);
|
||||
}
|
||||
}
|
||||
|
||||
// User-local binary directories
|
||||
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
if (!string.IsNullOrEmpty(home))
|
||||
{
|
||||
yield return Path.Combine(home, ".local", "bin", exeName);
|
||||
yield return Path.Combine(home, ".cargo", "bin", exeName);
|
||||
}
|
||||
|
||||
// System directories (platform-specific)
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
yield return "/opt/homebrew/bin/" + exeName;
|
||||
yield return "/usr/local/bin/" + exeName;
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
yield return "/usr/local/bin/" + exeName;
|
||||
yield return "/usr/bin/" + exeName;
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
|
||||
|
||||
if (!string.IsNullOrEmpty(localAppData))
|
||||
{
|
||||
yield return Path.Combine(localAppData, "Programs", "uv", exeName);
|
||||
// WinGet creates shim files in this location
|
||||
yield return Path.Combine(localAppData, "Microsoft", "WinGet", "Links", exeName);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(programFiles))
|
||||
{
|
||||
yield return Path.Combine(programFiles, "uv", exeName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,6 +175,10 @@
|
|||
background-color: rgba(200, 50, 50, 1);
|
||||
}
|
||||
|
||||
.status-indicator-small.warning {
|
||||
background-color: rgba(255, 200, 0, 1);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.action-button {
|
||||
min-width: 80px;
|
||||
|
|
|
|||
|
|
@ -190,16 +190,51 @@ namespace MCPForUnity.Editor.Windows.Components.Settings
|
|||
var pathService = MCPServiceLocator.Paths;
|
||||
|
||||
bool hasOverride = pathService.HasUvxPathOverride;
|
||||
bool hasFallback = pathService.HasUvxPathFallback;
|
||||
string uvxPath = hasOverride ? pathService.GetUvxPath() : null;
|
||||
uvxPathOverride.value = hasOverride
|
||||
? (uvxPath ?? "(override set but invalid)")
|
||||
: "uvx (uses PATH)";
|
||||
|
||||
// Determine display text based on override and fallback status
|
||||
if (hasOverride)
|
||||
{
|
||||
if (hasFallback)
|
||||
{
|
||||
// Override path invalid, using system fallback
|
||||
string overridePath = EditorPrefs.GetString(EditorPrefKeys.UvxPathOverride, string.Empty);
|
||||
uvxPathOverride.value = $"Invalid override path: {overridePath} (fallback to uvx path) {uvxPath}";
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(uvxPath))
|
||||
{
|
||||
// Override path valid
|
||||
uvxPathOverride.value = uvxPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Override set but invalid, no fallback available
|
||||
string overridePath = EditorPrefs.GetString(EditorPrefKeys.UvxPathOverride, string.Empty);
|
||||
uvxPathOverride.value = $"Invalid override path: {overridePath}, no uv found";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
uvxPathOverride.value = "uvx (uses PATH)";
|
||||
}
|
||||
|
||||
uvxPathStatus.RemoveFromClassList("valid");
|
||||
uvxPathStatus.RemoveFromClassList("invalid");
|
||||
uvxPathStatus.RemoveFromClassList("warning");
|
||||
|
||||
if (hasOverride)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(uvxPath) && File.Exists(uvxPath))
|
||||
if (hasFallback)
|
||||
{
|
||||
// Using fallback - show as warning (yellow)
|
||||
uvxPathStatus.AddToClassList("warning");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Override mode: validate the override path
|
||||
string overridePath = EditorPrefs.GetString(EditorPrefKeys.UvxPathOverride, string.Empty);
|
||||
if (pathService.TryValidateUvxExecutable(overridePath, out _))
|
||||
{
|
||||
uvxPathStatus.AddToClassList("valid");
|
||||
}
|
||||
|
|
@ -208,10 +243,20 @@ namespace MCPForUnity.Editor.Windows.Components.Settings
|
|||
uvxPathStatus.AddToClassList("invalid");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// PATH mode: validate system uvx
|
||||
string systemUvxPath = pathService.GetUvxPath();
|
||||
if (!string.IsNullOrEmpty(systemUvxPath) && pathService.TryValidateUvxExecutable(systemUvxPath, out _))
|
||||
{
|
||||
uvxPathStatus.AddToClassList("valid");
|
||||
}
|
||||
else
|
||||
{
|
||||
uvxPathStatus.AddToClassList("invalid");
|
||||
}
|
||||
}
|
||||
|
||||
gitUrlOverride.value = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");
|
||||
devModeForceRefreshToggle.value = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
<ui:VisualElement class="advanced-settings-content">
|
||||
<ui:Label text="Path Overrides (leave empty for auto-detection):" class="advanced-label" />
|
||||
<ui:VisualElement class="override-row">
|
||||
<ui:Label text="UV Path:" class="override-label" />
|
||||
<ui:Label text="UVX Path:" class="override-label" />
|
||||
<ui:VisualElement class="status-indicator-small" name="uv-path-status" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement class="path-override-controls">
|
||||
|
|
|
|||
Loading…
Reference in New Issue