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()
{ {
command = uvPath, if (args == null) return null;
args = new[] { "--directory", pythonDir, "run", "server.py" }, for (int i = 0; i < args.Length - 1; i++)
};
if (mcpClient?.mcpType == McpTypes.VSCode)
{ {
unityMCPConfig.type = "stdio"; if (string.Equals(args[i], "--directory", StringComparison.OrdinalIgnoreCase))
{
return args[i + 1];
} }
}
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,121 +1006,81 @@ 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:
// VSCode-specific configuration (top-level "servers")
if (existingConfig.servers == null)
{
existingConfig.servers = new Newtonsoft.Json.Linq.JObject();
}
// Add/update UnityMCP server in VSCode mcp.json
existingConfig.servers.unityMCP =
JsonConvert.DeserializeObject<Newtonsoft.Json.Linq.JToken>(
JsonConvert.SerializeObject(unityMCPConfig)
);
break;
default:
// Standard MCP configuration (Claude Desktop, Cursor, etc.)
// Ensure mcpServers object exists
if (existingConfig.mcpServers == null)
{
existingConfig.mcpServers = new Newtonsoft.Json.Linq.JObject();
}
// Add/update UnityMCP server in standard MCP settings
existingConfig.mcpServers.unityMCP =
JsonConvert.DeserializeObject<Newtonsoft.Json.Linq.JToken>(
JsonConvert.SerializeObject(unityMCPConfig)
);
break;
}
// If config already has a working absolute uv path, avoid rewriting it on refresh
try try
{ {
if (mcpClient?.mcpType != McpTypes.ClaudeCode) if (isVSCode)
{
// Inspect existing command for stability (Windows absolute path that exists)
string existingCommand = null;
if (mcpClient?.mcpType == McpTypes.VSCode)
{ {
existingCommand = existingConfig?.servers?.unityMCP?.command?.ToString(); existingCommand = existingConfig?.servers?.unityMCP?.command?.ToString();
existingArgs = existingConfig?.servers?.unityMCP?.args?.ToObject<string[]>();
} }
else else
{ {
existingCommand = existingConfig?.mcpServers?.unityMCP?.command?.ToString(); existingCommand = existingConfig?.mcpServers?.unityMCP?.command?.ToString();
existingArgs = existingConfig?.mcpServers?.unityMCP?.args?.ToObject<string[]>();
}
}
catch { }
// 1) Start from existing, only fill gaps
string uvPath = IsValidUv(existingCommand) ? existingCommand : FindUvPath();
if (uvPath == null) return "UV package manager not found. Please install UV first.";
string serverSrc = ExtractDirectoryArg(existingArgs);
bool serverValid = !string.IsNullOrEmpty(serverSrc)
&& System.IO.File.Exists(System.IO.Path.Combine(serverSrc, "server.py"));
if (!serverValid)
{
serverSrc = ResolveServerSrc();
} }
if (!string.IsNullOrEmpty(existingCommand)) // Hard-block PackageCache on Windows unless dev override is set
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
&& serverSrc.IndexOf(@"\Library\PackageCache\", StringComparison.OrdinalIgnoreCase) >= 0
&& !UnityEditor.EditorPrefs.GetBool("UnityMCP.UseEmbeddedServer", false))
{ {
bool keep = false; serverSrc = ServerInstaller.GetServerPath();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Consider absolute, existing paths as stable; prefer WinGet Links
if (Path.IsPathRooted(existingCommand) && File.Exists(existingCommand))
{
keep = true;
} }
// 2) Canonical args order
var newArgs = new[] { "run", "--directory", serverSrc, "server.py" };
// 3) Only write if changed
bool changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal)
|| !ArgsEqual(existingArgs, newArgs);
if (!changed)
{
return "Configured successfully"; // nothing to do
}
// 4) Ensure containers exist and write back minimal changes
if (isVSCode)
{
if (existingConfig.servers == null) existingConfig.servers = new Newtonsoft.Json.Linq.JObject();
if (existingConfig.servers.unityMCP == null) existingConfig.servers.unityMCP = new Newtonsoft.Json.Linq.JObject();
existingConfig.servers.unityMCP.command = uvPath;
existingConfig.servers.unityMCP.args = Newtonsoft.Json.Linq.JArray.FromObject(newArgs);
existingConfig.servers.unityMCP.type = "stdio";
} }
else else
{ {
// On Unix, keep absolute existing path as well if (existingConfig.mcpServers == null) existingConfig.mcpServers = new Newtonsoft.Json.Linq.JObject();
if (Path.IsPathRooted(existingCommand) && File.Exists(existingCommand)) if (existingConfig.mcpServers.unityMCP == null) existingConfig.mcpServers.unityMCP = new Newtonsoft.Json.Linq.JObject();
{ existingConfig.mcpServers.unityMCP.command = uvPath;
keep = true; existingConfig.mcpServers.unityMCP.args = Newtonsoft.Json.Linq.JArray.FromObject(newArgs);
}
} }
if (keep)
{
// 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();
}
existingConfig.servers.unityMCP.args =
JsonConvert.DeserializeObject<Newtonsoft.Json.Linq.JToken>(
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";
} }
@ -1182,9 +1159,38 @@ namespace UnityMcpBridge.Editor.Windows
ManualConfigEditorWindow.ShowWindow(configPath, manualConfigJson, mcpClient); ManualConfigEditorWindow.ShowWindow(configPath, manualConfigJson, mcpClient);
} }
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() private string FindPackagePythonDirectory()
{ {
string pythonDir = ServerInstaller.GetServerPath(); string pythonDir = ResolveServerSrc();
try try
{ {
@ -1211,16 +1217,24 @@ 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 (UnityEditor.EditorPrefs.GetBool("UnityMCP.UseEmbeddedServer", false))
{
if (ServerPathResolver.TryFindEmbeddedServerSource(out string embedded)) if (ServerPathResolver.TryFindEmbeddedServerSource(out string embedded))
{ {
return 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)