Skip to content

Commit c3a8dab

Browse files
committed
Fix Claude uvx path resolution
1 parent ab86d5b commit c3a8dab

File tree

3 files changed

+118
-1
lines changed

3 files changed

+118
-1
lines changed

MCPForUnity/Editor/Clients/Configurators/ClaudeDesktopConfigurator.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ public ClaudeDesktopConfigurator() : base(new McpClient
1515
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Claude", "claude_desktop_config.json"),
1616
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Claude", "claude_desktop_config.json"),
1717
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Claude", "claude_desktop_config.json"),
18-
SupportsHttpTransport = false
18+
SupportsHttpTransport = false,
19+
StripEnvWhenNotRequired = true
1920
})
2021
{ }
2122

MCPForUnity/Editor/Services/PathResolverService.cs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Diagnostics;
34
using System.IO;
45
using System.Linq;
@@ -34,6 +35,12 @@ public string GetUvxPath()
3435
McpLog.Debug("No uvx path override found, falling back to default command");
3536
}
3637

38+
string discovered = ResolveUvxFromSystem();
39+
if (!string.IsNullOrEmpty(discovered))
40+
{
41+
return discovered;
42+
}
43+
3744
return "uvx";
3845
}
3946

@@ -123,6 +130,81 @@ public bool IsClaudeCliDetected()
123130
return !string.IsNullOrEmpty(GetClaudeCliPath());
124131
}
125132

133+
private static string ResolveUvxFromSystem()
134+
{
135+
try
136+
{
137+
foreach (string candidate in EnumerateUvxCandidates())
138+
{
139+
if (!string.IsNullOrEmpty(candidate) && File.Exists(candidate))
140+
{
141+
return candidate;
142+
}
143+
}
144+
}
145+
catch
146+
{
147+
// fall back to bare command
148+
}
149+
150+
return null;
151+
}
152+
153+
private static IEnumerable<string> EnumerateUvxCandidates()
154+
{
155+
string exeName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "uvx.exe" : "uvx";
156+
157+
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
158+
if (!string.IsNullOrEmpty(home))
159+
{
160+
yield return Path.Combine(home, ".local", "bin", exeName);
161+
yield return Path.Combine(home, ".cargo", "bin", exeName);
162+
}
163+
164+
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
165+
{
166+
yield return "/opt/homebrew/bin/" + exeName;
167+
yield return "/usr/local/bin/" + exeName;
168+
}
169+
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
170+
{
171+
yield return "/usr/local/bin/" + exeName;
172+
yield return "/usr/bin/" + exeName;
173+
}
174+
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
175+
{
176+
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
177+
string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
178+
179+
if (!string.IsNullOrEmpty(localAppData))
180+
{
181+
yield return Path.Combine(localAppData, "Programs", "uv", exeName);
182+
}
183+
184+
if (!string.IsNullOrEmpty(programFiles))
185+
{
186+
yield return Path.Combine(programFiles, "uv", exeName);
187+
}
188+
}
189+
190+
string pathEnv = Environment.GetEnvironmentVariable("PATH");
191+
if (!string.IsNullOrEmpty(pathEnv))
192+
{
193+
foreach (string rawDir in pathEnv.Split(Path.PathSeparator))
194+
{
195+
if (string.IsNullOrWhiteSpace(rawDir)) continue;
196+
string dir = rawDir.Trim();
197+
yield return Path.Combine(dir, exeName);
198+
199+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
200+
{
201+
// Some PATH entries may already contain the file without extension
202+
yield return Path.Combine(dir, "uvx");
203+
}
204+
}
205+
}
206+
}
207+
126208
public void SetUvxPathOverride(string path)
127209
{
128210
if (string.IsNullOrEmpty(path))

TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/WriteToConfigTests.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,40 @@ public void DoesNotAddEnvOrDisabled_ForTrae()
167167
AssertTransportConfiguration(unity, client);
168168
}
169169

170+
[Test]
171+
public void ClaudeDesktop_UsesAbsoluteUvPath_WhenOverrideProvided()
172+
{
173+
var configPath = Path.Combine(_tempRoot, "claude-desktop.json");
174+
WriteInitialConfig(configPath, isVSCode: false, command: "uvx", directory: "/old/path");
175+
176+
WithTransportPreference(false, () =>
177+
{
178+
EditorPrefs.SetString(EditorPrefKeys.UvxPathOverride, "/abs/mock/uvx");
179+
try
180+
{
181+
var client = new McpClient
182+
{
183+
name = "Claude Desktop",
184+
SupportsHttpTransport = false,
185+
StripEnvWhenNotRequired = true
186+
};
187+
188+
InvokeWriteToConfig(configPath, client);
189+
190+
var root = JObject.Parse(File.ReadAllText(configPath));
191+
var unity = (JObject)root.SelectToken("mcpServers.unityMCP");
192+
Assert.NotNull(unity, "Expected mcpServers.unityMCP node");
193+
Assert.AreEqual("/abs/mock/uvx", (string)unity["command"], "Claude Desktop should use absolute uvx path");
194+
Assert.IsNull(unity["env"], "Claude Desktop config should not include env block when not required");
195+
AssertTransportConfiguration(unity, client);
196+
}
197+
finally
198+
{
199+
EditorPrefs.DeleteKey(EditorPrefKeys.UvxPathOverride);
200+
}
201+
});
202+
}
203+
170204
[Test]
171205
public void PreservesExistingEnvAndDisabled_ForKiro()
172206
{

0 commit comments

Comments
 (0)