feat: preserve existing client config; prefer installed server path; add ResolveServerSrc; block PackageCache unless dev override; canonical uv args

main
David Sarno 2025-08-13 17:46:07 -07:00
parent a7af0cd9b0
commit 6e22721d3a
1 changed files with 152 additions and 138 deletions

View File

@ -931,24 +931,41 @@ namespace UnityMcpBridge.Editor.Windows
Repaint(); Repaint();
} }
private string WriteToConfig(string pythonDir, string configPath, McpClient mcpClient = null) private static bool IsValidUv(string path)
{ {
string uvPath = FindUvPath(); return !string.IsNullOrEmpty(path)
if (uvPath == null) && System.IO.Path.IsPathRooted(path)
{ && System.IO.File.Exists(path);
return "UV package manager not found. Please install UV first."; }
}
// Create configuration object for unityMCP private static string ExtractDirectoryArg(string[] args)
McpConfigServer unityMCPConfig = new() {
{ if (args == null) return null;
command = uvPath, for (int i = 0; i < args.Length - 1; i++)
args = new[] { "--directory", pythonDir, "run", "server.py" }, {
}; if (string.Equals(args[i], "--directory", StringComparison.OrdinalIgnoreCase))
if (mcpClient?.mcpType == McpTypes.VSCode) {
{ return args[i + 1];
unityMCPConfig.type = "stdio"; }
} }
return null;
}
private static bool ArgsEqual(string[] a, string[] b)
{
if (a == null || b == null) return a == b;
if (a.Length != b.Length) return false;
for (int i = 0; i < a.Length; i++)
{
if (!string.Equals(a[i], b[i], StringComparison.Ordinal)) return false;
}
return true;
}
private string WriteToConfig(string pythonDir, string configPath, McpClient mcpClient = null)
{
// 0) Respect explicit lock (hidden pref or UI toggle)
try { if (UnityEditor.EditorPrefs.GetBool("UnityMCP.LockCursorConfig", false)) return "Skipped (locked)"; } catch { }
JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented }; JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented };
@ -989,123 +1006,83 @@ namespace UnityMcpBridge.Editor.Windows
existingConfig = new Newtonsoft.Json.Linq.JObject(); existingConfig = new Newtonsoft.Json.Linq.JObject();
} }
// Handle different client types with a switch statement // Determine existing entry references (command/args)
//Comments: Interestingly, VSCode has mcp.servers.unityMCP while others have mcpServers.unityMCP, which is why we need to prevent this string existingCommand = null;
switch (mcpClient?.mcpType) string[] existingArgs = null;
{ bool isVSCode = (mcpClient?.mcpType == McpTypes.VSCode);
case McpTypes.VSCode: try
// VSCode-specific configuration (top-level "servers") {
if (existingConfig.servers == null) if (isVSCode)
{ {
existingConfig.servers = new Newtonsoft.Json.Linq.JObject(); existingCommand = existingConfig?.servers?.unityMCP?.command?.ToString();
} existingArgs = existingConfig?.servers?.unityMCP?.args?.ToObject<string[]>();
}
else
{
existingCommand = existingConfig?.mcpServers?.unityMCP?.command?.ToString();
existingArgs = existingConfig?.mcpServers?.unityMCP?.args?.ToObject<string[]>();
}
}
catch { }
// Add/update UnityMCP server in VSCode mcp.json // 1) Start from existing, only fill gaps
existingConfig.servers.unityMCP = string uvPath = IsValidUv(existingCommand) ? existingCommand : FindUvPath();
JsonConvert.DeserializeObject<Newtonsoft.Json.Linq.JToken>( if (uvPath == null) return "UV package manager not found. Please install UV first.";
JsonConvert.SerializeObject(unityMCPConfig)
);
break;
default: string serverSrc = ExtractDirectoryArg(existingArgs);
// Standard MCP configuration (Claude Desktop, Cursor, etc.) bool serverValid = !string.IsNullOrEmpty(serverSrc)
// Ensure mcpServers object exists && System.IO.File.Exists(System.IO.Path.Combine(serverSrc, "server.py"));
if (existingConfig.mcpServers == null) if (!serverValid)
{ {
existingConfig.mcpServers = new Newtonsoft.Json.Linq.JObject(); serverSrc = ResolveServerSrc();
} }
// Add/update UnityMCP server in standard MCP settings // Hard-block PackageCache on Windows unless dev override is set
existingConfig.mcpServers.unityMCP = if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
JsonConvert.DeserializeObject<Newtonsoft.Json.Linq.JToken>( && serverSrc.IndexOf(@"\Library\PackageCache\", StringComparison.OrdinalIgnoreCase) >= 0
JsonConvert.SerializeObject(unityMCPConfig) && !UnityEditor.EditorPrefs.GetBool("UnityMCP.UseEmbeddedServer", false))
); {
break; serverSrc = ServerInstaller.GetServerPath();
} }
// If config already has a working absolute uv path, avoid rewriting it on refresh // 2) Canonical args order
try var newArgs = new[] { "run", "--directory", serverSrc, "server.py" };
{
if (mcpClient?.mcpType != McpTypes.ClaudeCode)
{
// Inspect existing command for stability (Windows absolute path that exists)
string existingCommand = null;
if (mcpClient?.mcpType == McpTypes.VSCode)
{
existingCommand = existingConfig?.servers?.unityMCP?.command?.ToString();
}
else
{
existingCommand = existingConfig?.mcpServers?.unityMCP?.command?.ToString();
}
if (!string.IsNullOrEmpty(existingCommand)) // 3) Only write if changed
{ bool changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal)
bool keep = false; || !ArgsEqual(existingArgs, newArgs);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (!changed)
{ {
// Consider absolute, existing paths as stable; prefer WinGet Links return "Configured successfully"; // nothing to do
if (Path.IsPathRooted(existingCommand) && File.Exists(existingCommand)) }
{
keep = true;
}
}
else
{
// On Unix, keep absolute existing path as well
if (Path.IsPathRooted(existingCommand) && File.Exists(existingCommand))
{
keep = true;
}
}
if (keep) // 4) Ensure containers exist and write back minimal changes
{ if (isVSCode)
// Merge without replacing the existing command {
if (mcpClient?.mcpType == McpTypes.VSCode) if (existingConfig.servers == null) existingConfig.servers = new Newtonsoft.Json.Linq.JObject();
{ if (existingConfig.servers.unityMCP == null) existingConfig.servers.unityMCP = new Newtonsoft.Json.Linq.JObject();
if (existingConfig.servers == null) existingConfig.servers.unityMCP.command = uvPath;
{ existingConfig.servers.unityMCP.args = Newtonsoft.Json.Linq.JArray.FromObject(newArgs);
existingConfig.servers = new Newtonsoft.Json.Linq.JObject(); existingConfig.servers.unityMCP.type = "stdio";
} }
if (existingConfig.servers.unityMCP == null) else
{ {
existingConfig.servers.unityMCP = new Newtonsoft.Json.Linq.JObject(); if (existingConfig.mcpServers == null) existingConfig.mcpServers = new Newtonsoft.Json.Linq.JObject();
} if (existingConfig.mcpServers.unityMCP == null) existingConfig.mcpServers.unityMCP = new Newtonsoft.Json.Linq.JObject();
existingConfig.servers.unityMCP.args = existingConfig.mcpServers.unityMCP.command = uvPath;
JsonConvert.DeserializeObject<Newtonsoft.Json.Linq.JToken>( existingConfig.mcpServers.unityMCP.args = Newtonsoft.Json.Linq.JArray.FromObject(newArgs);
JsonConvert.SerializeObject(unityMCPConfig.args) }
);
}
else
{
if (existingConfig.mcpServers == null)
{
existingConfig.mcpServers = new Newtonsoft.Json.Linq.JObject();
}
if (existingConfig.mcpServers.unityMCP == null)
{
existingConfig.mcpServers.unityMCP = new Newtonsoft.Json.Linq.JObject();
}
existingConfig.mcpServers.unityMCP.args =
JsonConvert.DeserializeObject<Newtonsoft.Json.Linq.JToken>(
JsonConvert.SerializeObject(unityMCPConfig.args)
);
}
string mergedKeep = JsonConvert.SerializeObject(existingConfig, jsonSettings);
File.WriteAllText(configPath, mergedKeep);
return "Configured successfully";
}
}
}
}
catch { /* fall back to normal write */ }
// Write the merged configuration back to file string mergedJson = JsonConvert.SerializeObject(existingConfig, jsonSettings);
string mergedJson = JsonConvert.SerializeObject(existingConfig, jsonSettings); File.WriteAllText(configPath, mergedJson);
File.WriteAllText(configPath, mergedJson); try
{
if (IsValidUv(uvPath)) UnityEditor.EditorPrefs.SetString("UnityMCP.UvPath", uvPath);
UnityEditor.EditorPrefs.SetString("UnityMCP.ServerSrc", serverSrc);
}
catch { }
return "Configured successfully"; return "Configured successfully";
} }
private void ShowManualConfigurationInstructions( private void ShowManualConfigurationInstructions(
@ -1182,9 +1159,38 @@ namespace UnityMcpBridge.Editor.Windows
ManualConfigEditorWindow.ShowWindow(configPath, manualConfigJson, mcpClient); ManualConfigEditorWindow.ShowWindow(configPath, manualConfigJson, mcpClient);
} }
private string FindPackagePythonDirectory() private static string ResolveServerSrc()
{
try
{
string remembered = UnityEditor.EditorPrefs.GetString("UnityMCP.ServerSrc", string.Empty);
if (!string.IsNullOrEmpty(remembered) && File.Exists(Path.Combine(remembered, "server.py")))
{
return remembered;
}
ServerInstaller.EnsureServerInstalled();
string installed = ServerInstaller.GetServerPath();
if (File.Exists(Path.Combine(installed, "server.py")))
{
return installed;
}
bool useEmbedded = UnityEditor.EditorPrefs.GetBool("UnityMCP.UseEmbeddedServer", false);
if (useEmbedded && ServerPathResolver.TryFindEmbeddedServerSource(out string embedded)
&& File.Exists(Path.Combine(embedded, "server.py")))
{
return embedded;
}
return installed;
}
catch { return ServerInstaller.GetServerPath(); }
}
private string FindPackagePythonDirectory()
{ {
string pythonDir = ServerInstaller.GetServerPath(); string pythonDir = ResolveServerSrc();
try try
{ {
@ -1211,17 +1217,25 @@ namespace UnityMcpBridge.Editor.Windows
} }
} }
// Resolve via shared helper (handles local registry and older fallback) // Resolve via shared helper (handles local registry and older fallback) only if dev override on
if (ServerPathResolver.TryFindEmbeddedServerSource(out string embedded)) if (UnityEditor.EditorPrefs.GetBool("UnityMCP.UseEmbeddedServer", false))
{ {
return embedded; if (ServerPathResolver.TryFindEmbeddedServerSource(out string embedded))
} {
return embedded;
}
}
// If still not found, return the placeholder path // Log only if the resolved path does not actually contain server.py
if (debugLogsEnabled) if (debugLogsEnabled)
{ {
UnityEngine.Debug.LogWarning("Could not find Python directory, using placeholder path"); bool hasServer = false;
} try { hasServer = File.Exists(Path.Combine(pythonDir, "server.py")); } catch { }
if (!hasServer)
{
UnityEngine.Debug.LogWarning("Could not find Python directory with server.py; falling back to installed path");
}
}
} }
catch (Exception e) catch (Exception e)
{ {