diff --git a/MCPForUnity/Editor/Data/DefaultServerConfig.cs b/MCPForUnity/Editor/Data/DefaultServerConfig.cs
deleted file mode 100644
index 59cced75..00000000
--- a/MCPForUnity/Editor/Data/DefaultServerConfig.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using MCPForUnity.Editor.Models;
-
-namespace MCPForUnity.Editor.Data
-{
- public class DefaultServerConfig : ServerConfig
- {
- public new string unityHost = "localhost";
- public new int unityPort = 6400;
- public new int mcpPort = 6500;
- public new float connectionTimeout = 15.0f;
- public new int bufferSize = 32768;
- public new string logLevel = "INFO";
- public new string logFormat = "%(asctime)s - %(name)s - %(levelname)s - %(message)s";
- public new int maxRetries = 3;
- public new float retryDelay = 1.0f;
- }
-}
diff --git a/MCPForUnity/Editor/Dependencies/DependencyManager.cs b/MCPForUnity/Editor/Dependencies/DependencyManager.cs
index ce6efef2..d5a082a1 100644
--- a/MCPForUnity/Editor/Dependencies/DependencyManager.cs
+++ b/MCPForUnity/Editor/Dependencies/DependencyManager.cs
@@ -126,7 +126,7 @@ private static void GenerateRecommendations(DependencyCheckResult result, IPlatf
{
if (dep.Name == "Python")
{
- result.RecommendedActions.Add($"Install Python 3.10+ from: {detector.GetPythonInstallUrl()}");
+ result.RecommendedActions.Add($"Install Python 3.11+ from: {detector.GetPythonInstallUrl()}");
}
else if (dep.Name == "UV Package Manager")
{
diff --git a/MCPForUnity/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs b/MCPForUnity/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs
index f654612c..a1281747 100644
--- a/MCPForUnity/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs
+++ b/MCPForUnity/Editor/Dependencies/PlatformDetectors/LinuxPlatformDetector.cs
@@ -62,7 +62,7 @@ public override DependencyStatus DetectPython()
}
}
- status.ErrorMessage = "Python not found. Please install Python 3.10 or later.";
+ status.ErrorMessage = "Python not found. Please install Python 3.11 or later.";
status.Details = "Checked common installation paths including system, snap, and user-local locations.";
}
catch (Exception ex)
@@ -144,10 +144,10 @@ private bool TryValidatePython(string pythonPath, out string version, out string
version = output.Substring(7); // Remove "Python " prefix
fullPath = pythonPath;
- // Validate minimum version (Python 4+ or Python 3.10+)
+ // Validate minimum version (Python 4+ or Python 3.11+)
if (TryParseVersion(version, out var major, out var minor))
{
- return major > 3 || (major >= 3 && minor >= 10);
+ return major > 3 || (major >= 3 && minor >= 11);
}
}
}
diff --git a/MCPForUnity/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs b/MCPForUnity/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs
index c9d152d8..64e9a50a 100644
--- a/MCPForUnity/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs
+++ b/MCPForUnity/Editor/Dependencies/PlatformDetectors/MacOSPlatformDetector.cs
@@ -35,8 +35,7 @@ public override DependencyStatus DetectPython()
"/opt/homebrew/bin/python3",
"/Library/Frameworks/Python.framework/Versions/3.13/bin/python3",
"/Library/Frameworks/Python.framework/Versions/3.12/bin/python3",
- "/Library/Frameworks/Python.framework/Versions/3.11/bin/python3",
- "/Library/Frameworks/Python.framework/Versions/3.10/bin/python3"
+ "/Library/Frameworks/Python.framework/Versions/3.11/bin/python3"
};
foreach (var candidate in candidates)
@@ -65,7 +64,7 @@ public override DependencyStatus DetectPython()
}
}
- status.ErrorMessage = "Python not found. Please install Python 3.10 or later.";
+ status.ErrorMessage = "Python not found. Please install Python 3.11 or later.";
status.Details = "Checked common installation paths including Homebrew, Framework, and system locations.";
}
catch (Exception ex)
@@ -144,10 +143,10 @@ private bool TryValidatePython(string pythonPath, out string version, out string
version = output.Substring(7); // Remove "Python " prefix
fullPath = pythonPath;
- // Validate minimum version (Python 4+ or Python 3.10+)
+ // Validate minimum version (Python 4+ or Python 3.11+)
if (TryParseVersion(version, out var major, out var minor))
{
- return major > 3 || (major >= 3 && minor >= 10);
+ return major > 3 || (major >= 3 && minor >= 11);
}
}
}
diff --git a/MCPForUnity/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs b/MCPForUnity/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs
index 7891c6e3..bcb2c7d4 100644
--- a/MCPForUnity/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs
+++ b/MCPForUnity/Editor/Dependencies/PlatformDetectors/WindowsPlatformDetector.cs
@@ -68,7 +68,7 @@ public override DependencyStatus DetectPython()
}
}
- status.ErrorMessage = "Python not found. Please install Python 3.10 or later.";
+ status.ErrorMessage = "Python not found. Please install Python 3.11 or later.";
status.Details = "Checked common installation paths and PATH environment variable.";
}
catch (Exception ex)
@@ -132,10 +132,10 @@ private bool TryValidatePython(string pythonPath, out string version, out string
version = output.Substring(7); // Remove "Python " prefix
fullPath = pythonPath;
- // Validate minimum version (Python 4+ or Python 3.10+)
+ // Validate minimum version (Python 4+ or Python 3.11+)
if (TryParseVersion(version, out var major, out var minor))
{
- return major > 3 || (major >= 3 && minor >= 10);
+ return major > 3 || (major >= 3 && minor >= 11);
}
}
}
diff --git a/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs b/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs
index d3d77dc3..a4728901 100644
--- a/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs
+++ b/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs
@@ -3,6 +3,7 @@
using System.IO;
using System.Linq;
using MCPForUnity.External.Tommy;
+using MCPForUnity.Editor.Services;
namespace MCPForUnity.Editor.Helpers
{
@@ -26,10 +27,10 @@ public static bool IsCodexConfigured(string pythonDir)
string toml = File.ReadAllText(configPath);
if (!TryParseCodexServer(toml, out _, out var args)) return false;
- string dir = McpConfigFileHelper.ExtractDirectoryArg(args);
+ string dir = McpConfigurationHelper.ExtractDirectoryArg(args);
if (string.IsNullOrEmpty(dir)) return false;
- return McpConfigFileHelper.PathsEqual(dir, pythonDir);
+ return McpConfigurationHelper.PathsEqual(dir, pythonDir);
}
catch
{
@@ -125,6 +126,8 @@ private static TomlTable TryParseToml(string toml)
///
/// Creates a TomlTable for the unityMCP server configuration
///
+ /// Path to uv executable
+ /// Path to server source directory
private static TomlTable CreateUnityMcpTable(string uvPath, string serverSrc)
{
var unityMCP = new TomlTable();
@@ -137,6 +140,15 @@ private static TomlTable CreateUnityMcpTable(string uvPath, string serverSrc)
argsArray.Add(new TomlString { Value = "server.py" });
unityMCP["args"] = argsArray;
+ // Add Windows-specific environment configuration, see: https://github.com/CoplayDev/unity-mcp/issues/315
+ var platformService = MCPServiceLocator.Platform;
+ if (platformService.IsWindows())
+ {
+ var envTable = new TomlTable { IsInline = true };
+ envTable["SystemRoot"] = new TomlString { Value = platformService.GetSystemRoot() };
+ unityMCP["env"] = envTable;
+ }
+
return unityMCP;
}
diff --git a/MCPForUnity/Editor/Helpers/McpConfigFileHelper.cs b/MCPForUnity/Editor/Helpers/McpConfigFileHelper.cs
deleted file mode 100644
index 389d47d2..00000000
--- a/MCPForUnity/Editor/Helpers/McpConfigFileHelper.cs
+++ /dev/null
@@ -1,186 +0,0 @@
-using System;
-using System.IO;
-using System.Runtime.InteropServices;
-using System.Text;
-using UnityEditor;
-
-namespace MCPForUnity.Editor.Helpers
-{
- ///
- /// Shared helpers for reading and writing MCP client configuration files.
- /// Consolidates file atomics and server directory resolution so the editor
- /// window can focus on UI concerns only.
- ///
- public static class McpConfigFileHelper
- {
- public static string ExtractDirectoryArg(string[] args)
- {
- if (args == null) return null;
- for (int i = 0; i < args.Length - 1; i++)
- {
- if (string.Equals(args[i], "--directory", StringComparison.OrdinalIgnoreCase))
- {
- return args[i + 1];
- }
- }
- return null;
- }
-
- public static bool PathsEqual(string a, string b)
- {
- if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return false;
- try
- {
- string na = Path.GetFullPath(a.Trim());
- string nb = Path.GetFullPath(b.Trim());
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- return string.Equals(na, nb, StringComparison.OrdinalIgnoreCase);
- }
- return string.Equals(na, nb, StringComparison.Ordinal);
- }
- catch
- {
- return false;
- }
- }
-
- ///
- /// Resolves the server directory to use for MCP tools, preferring
- /// existing config values and falling back to installed/embedded copies.
- ///
- public static string ResolveServerDirectory(string pythonDir, string[] existingArgs)
- {
- string serverSrc = ExtractDirectoryArg(existingArgs);
- bool serverValid = !string.IsNullOrEmpty(serverSrc)
- && File.Exists(Path.Combine(serverSrc, "server.py"));
- if (!serverValid)
- {
- if (!string.IsNullOrEmpty(pythonDir)
- && File.Exists(Path.Combine(pythonDir, "server.py")))
- {
- serverSrc = pythonDir;
- }
- else
- {
- serverSrc = ResolveServerSource();
- }
- }
-
- try
- {
- if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && !string.IsNullOrEmpty(serverSrc))
- {
- string norm = serverSrc.Replace('\\', '/');
- int idx = norm.IndexOf("/.local/share/UnityMCP/", StringComparison.Ordinal);
- if (idx >= 0)
- {
- string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty;
- string suffix = norm.Substring(idx + "/.local/share/".Length);
- serverSrc = Path.Combine(home, "Library", "Application Support", suffix);
- }
- }
- }
- catch
- {
- // Ignore failures and fall back to the original path.
- }
-
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
- && !string.IsNullOrEmpty(serverSrc)
- && serverSrc.IndexOf(@"\Library\PackageCache\", StringComparison.OrdinalIgnoreCase) >= 0
- && !EditorPrefs.GetBool("MCPForUnity.UseEmbeddedServer", false))
- {
- serverSrc = ServerInstaller.GetServerPath();
- }
-
- return serverSrc;
- }
-
- public static void WriteAtomicFile(string path, string contents)
- {
- string tmp = path + ".tmp";
- string backup = path + ".backup";
- bool writeDone = false;
- try
- {
- File.WriteAllText(tmp, contents, new UTF8Encoding(false));
- try
- {
- File.Replace(tmp, path, backup);
- writeDone = true;
- }
- catch (FileNotFoundException)
- {
- File.Move(tmp, path);
- writeDone = true;
- }
- catch (PlatformNotSupportedException)
- {
- if (File.Exists(path))
- {
- try
- {
- if (File.Exists(backup)) File.Delete(backup);
- }
- catch { }
- File.Move(path, backup);
- }
- File.Move(tmp, path);
- writeDone = true;
- }
- }
- catch (Exception ex)
- {
- try
- {
- if (!writeDone && File.Exists(backup))
- {
- try { File.Copy(backup, path, true); } catch { }
- }
- }
- catch { }
- throw new Exception($"Failed to write config file '{path}': {ex.Message}", ex);
- }
- finally
- {
- try { if (File.Exists(tmp)) File.Delete(tmp); } catch { }
- try { if (writeDone && File.Exists(backup)) File.Delete(backup); } catch { }
- }
- }
-
- public static string ResolveServerSource()
- {
- try
- {
- string remembered = EditorPrefs.GetString("MCPForUnity.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 = EditorPrefs.GetBool("MCPForUnity.UseEmbeddedServer", false);
- if (useEmbedded
- && ServerPathResolver.TryFindEmbeddedServerSource(out string embedded)
- && File.Exists(Path.Combine(embedded, "server.py")))
- {
- return embedded;
- }
-
- return installed;
- }
- catch
- {
- return ServerInstaller.GetServerPath();
- }
- }
- }
-}
diff --git a/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs b/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs
index d88bdbc8..96ad7ec2 100644
--- a/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs
+++ b/MCPForUnity/Editor/Helpers/McpConfigurationHelper.cs
@@ -2,6 +2,7 @@
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
+using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEditor;
@@ -105,20 +106,9 @@ public static string WriteMcpConfiguration(string pythonDir, string configPath,
}
catch { }
if (uvPath == null) return "UV package manager not found. Please install UV first.";
- string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs);
+ string serverSrc = ResolveServerDirectory(pythonDir, existingArgs);
- // 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
+ // Ensure containers exist and write back configuration
JObject existingRoot;
if (existingConfig is JObject eo)
existingRoot = eo;
@@ -129,7 +119,8 @@ public static string WriteMcpConfiguration(string pythonDir, string configPath,
string mergedJson = JsonConvert.SerializeObject(existingRoot, jsonSettings);
- McpConfigFileHelper.WriteAtomicFile(configPath, mergedJson);
+ EnsureConfigDirectoryExists(configPath);
+ WriteAtomicFile(configPath, mergedJson);
try
{
@@ -190,24 +181,12 @@ public static string ConfigureCodexClient(string pythonDir, string configPath, M
return "UV package manager not found. Please install UV first.";
}
- string serverSrc = McpConfigFileHelper.ResolveServerDirectory(pythonDir, existingArgs);
- var newArgs = new[] { "run", "--directory", serverSrc, "server.py" };
-
- bool changed = true;
- if (!string.IsNullOrEmpty(existingCommand) && existingArgs != null)
- {
- changed = !string.Equals(existingCommand, uvPath, StringComparison.Ordinal)
- || !ArgsEqual(existingArgs, newArgs);
- }
-
- if (!changed)
- {
- return "Configured successfully";
- }
+ string serverSrc = ResolveServerDirectory(pythonDir, existingArgs);
string updatedToml = CodexConfigHelper.UpsertCodexServerBlock(existingToml, uvPath, serverSrc);
- McpConfigFileHelper.WriteAtomicFile(configPath, updatedToml);
+ EnsureConfigDirectoryExists(configPath);
+ WriteAtomicFile(configPath, updatedToml);
try
{
@@ -246,20 +225,6 @@ private static bool IsValidUvBinary(string path)
catch { return false; }
}
- ///
- /// Compares two string arrays for equality
- ///
- 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;
- }
-
///
/// Gets the appropriate config file path for the given MCP client based on OS
///
@@ -292,5 +257,175 @@ public static void EnsureConfigDirectoryExists(string configPath)
{
Directory.CreateDirectory(Path.GetDirectoryName(configPath));
}
+
+ public static string ExtractDirectoryArg(string[] args)
+ {
+ if (args == null) return null;
+ for (int i = 0; i < args.Length - 1; i++)
+ {
+ if (string.Equals(args[i], "--directory", StringComparison.OrdinalIgnoreCase))
+ {
+ return args[i + 1];
+ }
+ }
+ return null;
+ }
+
+ public static bool PathsEqual(string a, string b)
+ {
+ if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return false;
+ try
+ {
+ string na = Path.GetFullPath(a.Trim());
+ string nb = Path.GetFullPath(b.Trim());
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ return string.Equals(na, nb, StringComparison.OrdinalIgnoreCase);
+ }
+ return string.Equals(na, nb, StringComparison.Ordinal);
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Resolves the server directory to use for MCP tools, preferring
+ /// existing config values and falling back to installed/embedded copies.
+ ///
+ public static string ResolveServerDirectory(string pythonDir, string[] existingArgs)
+ {
+ string serverSrc = ExtractDirectoryArg(existingArgs);
+ bool serverValid = !string.IsNullOrEmpty(serverSrc)
+ && File.Exists(Path.Combine(serverSrc, "server.py"));
+ if (!serverValid)
+ {
+ if (!string.IsNullOrEmpty(pythonDir)
+ && File.Exists(Path.Combine(pythonDir, "server.py")))
+ {
+ serverSrc = pythonDir;
+ }
+ else
+ {
+ serverSrc = ResolveServerSource();
+ }
+ }
+
+ try
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && !string.IsNullOrEmpty(serverSrc))
+ {
+ string norm = serverSrc.Replace('\\', '/');
+ int idx = norm.IndexOf("/.local/share/UnityMCP/", StringComparison.Ordinal);
+ if (idx >= 0)
+ {
+ string home = Environment.GetFolderPath(Environment.SpecialFolder.Personal) ?? string.Empty;
+ string suffix = norm.Substring(idx + "/.local/share/".Length);
+ serverSrc = Path.Combine(home, "Library", "Application Support", suffix);
+ }
+ }
+ }
+ catch
+ {
+ // Ignore failures and fall back to the original path.
+ }
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
+ && !string.IsNullOrEmpty(serverSrc)
+ && serverSrc.IndexOf(@"\Library\PackageCache\", StringComparison.OrdinalIgnoreCase) >= 0
+ && !EditorPrefs.GetBool("MCPForUnity.UseEmbeddedServer", false))
+ {
+ serverSrc = ServerInstaller.GetServerPath();
+ }
+
+ return serverSrc;
+ }
+
+ public static void WriteAtomicFile(string path, string contents)
+ {
+ string tmp = path + ".tmp";
+ string backup = path + ".backup";
+ bool writeDone = false;
+ try
+ {
+ File.WriteAllText(tmp, contents, new UTF8Encoding(false));
+ try
+ {
+ File.Replace(tmp, path, backup);
+ writeDone = true;
+ }
+ catch (FileNotFoundException)
+ {
+ File.Move(tmp, path);
+ writeDone = true;
+ }
+ catch (PlatformNotSupportedException)
+ {
+ if (File.Exists(path))
+ {
+ try
+ {
+ if (File.Exists(backup)) File.Delete(backup);
+ }
+ catch { }
+ File.Move(path, backup);
+ }
+ File.Move(tmp, path);
+ writeDone = true;
+ }
+ }
+ catch (Exception ex)
+ {
+ try
+ {
+ if (!writeDone && File.Exists(backup))
+ {
+ try { File.Copy(backup, path, true); } catch { }
+ }
+ }
+ catch { }
+ throw new Exception($"Failed to write config file '{path}': {ex.Message}", ex);
+ }
+ finally
+ {
+ try { if (File.Exists(tmp)) File.Delete(tmp); } catch { }
+ try { if (writeDone && File.Exists(backup)) File.Delete(backup); } catch { }
+ }
+ }
+
+ public static string ResolveServerSource()
+ {
+ try
+ {
+ string remembered = EditorPrefs.GetString("MCPForUnity.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 = EditorPrefs.GetBool("MCPForUnity.UseEmbeddedServer", false);
+ if (useEmbedded
+ && ServerPathResolver.TryFindEmbeddedServerSource(out string embedded)
+ && File.Exists(Path.Combine(embedded, "server.py")))
+ {
+ return embedded;
+ }
+
+ return installed;
+ }
+ catch
+ {
+ return ServerInstaller.GetServerPath();
+ }
+ }
}
}
diff --git a/MCPForUnity/Editor/Helpers/McpPathResolver.cs b/MCPForUnity/Editor/Helpers/McpPathResolver.cs
index be1089f7..04082a94 100644
--- a/MCPForUnity/Editor/Helpers/McpPathResolver.cs
+++ b/MCPForUnity/Editor/Helpers/McpPathResolver.cs
@@ -20,7 +20,7 @@ public static class McpPathResolver
///
public static string FindPackagePythonDirectory(bool debugLogsEnabled = false)
{
- string pythonDir = McpConfigFileHelper.ResolveServerSource();
+ string pythonDir = McpConfigurationHelper.ResolveServerSource();
try
{
diff --git a/MCPForUnity/Editor/Helpers/PackageDetector.cs b/MCPForUnity/Editor/Helpers/PackageDetector.cs
deleted file mode 100644
index 59e22348..00000000
--- a/MCPForUnity/Editor/Helpers/PackageDetector.cs
+++ /dev/null
@@ -1,106 +0,0 @@
-using UnityEditor;
-using UnityEngine;
-
-namespace MCPForUnity.Editor.Helpers
-{
- ///
- /// Auto-runs legacy/older install detection on package load/update (log-only).
- /// Runs once per embedded server version using an EditorPrefs version-scoped key.
- ///
- [InitializeOnLoad]
- public static class PackageDetector
- {
- private const string DetectOnceFlagKeyPrefix = "MCPForUnity.LegacyDetectLogged:";
-
- static PackageDetector()
- {
- try
- {
- string pkgVer = ReadPackageVersionOrFallback();
- string key = DetectOnceFlagKeyPrefix + pkgVer;
-
- // Always force-run if legacy roots exist or canonical install is missing
- bool legacyPresent = LegacyRootsExist();
- bool canonicalMissing = !System.IO.File.Exists(System.IO.Path.Combine(ServerInstaller.GetServerPath(), "server.py"));
-
- if (!EditorPrefs.GetBool(key, false) || legacyPresent || canonicalMissing)
- {
- // Marshal the entire flow to the main thread. EnsureServerInstalled may touch Unity APIs.
- EditorApplication.delayCall += () =>
- {
- string error = null;
- System.Exception capturedEx = null;
- try
- {
- // Ensure any UnityEditor API usage inside runs on the main thread
- ServerInstaller.EnsureServerInstalled();
- }
- catch (System.Exception ex)
- {
- error = ex.Message;
- capturedEx = ex;
- }
-
- // Unity APIs must stay on main thread
- try { EditorPrefs.SetBool(key, true); } catch { }
- // Ensure prefs cleanup happens on main thread
- try { EditorPrefs.DeleteKey("MCPForUnity.ServerSrc"); } catch { }
- try { EditorPrefs.DeleteKey("MCPForUnity.PythonDirOverride"); } catch { }
-
- if (!string.IsNullOrEmpty(error))
- {
- McpLog.Info($"Server check: {error}. Download via Window > MCP For Unity if needed.", always: false);
- }
- };
- }
- }
- catch { /* ignore */ }
- }
-
- private static string ReadEmbeddedVersionOrFallback()
- {
- try
- {
- if (ServerPathResolver.TryFindEmbeddedServerSource(out var embeddedSrc))
- {
- var p = System.IO.Path.Combine(embeddedSrc, "server_version.txt");
- if (System.IO.File.Exists(p))
- return (System.IO.File.ReadAllText(p)?.Trim() ?? "unknown");
- }
- }
- catch { }
- return "unknown";
- }
-
- private static string ReadPackageVersionOrFallback()
- {
- try
- {
- var info = UnityEditor.PackageManager.PackageInfo.FindForAssembly(typeof(PackageDetector).Assembly);
- if (info != null && !string.IsNullOrEmpty(info.version)) return info.version;
- }
- catch { }
- // Fallback to embedded server version if package info unavailable
- return ReadEmbeddedVersionOrFallback();
- }
-
- private static bool LegacyRootsExist()
- {
- try
- {
- string home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile) ?? string.Empty;
- string[] roots =
- {
- System.IO.Path.Combine(home, ".config", "UnityMCP", "UnityMcpServer", "src"),
- System.IO.Path.Combine(home, ".local", "share", "UnityMCP", "UnityMcpServer", "src")
- };
- foreach (var r in roots)
- {
- try { if (System.IO.File.Exists(System.IO.Path.Combine(r, "server.py"))) return true; } catch { }
- }
- }
- catch { }
- return false;
- }
- }
-}
diff --git a/MCPForUnity/Editor/Helpers/PackageInstaller.cs b/MCPForUnity/Editor/Helpers/PackageInstaller.cs
deleted file mode 100644
index 1d46f321..00000000
--- a/MCPForUnity/Editor/Helpers/PackageInstaller.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using UnityEditor;
-using UnityEngine;
-
-namespace MCPForUnity.Editor.Helpers
-{
- ///
- /// Handles automatic installation of the MCP server when the package is first installed.
- ///
- [InitializeOnLoad]
- public static class PackageInstaller
- {
- private const string InstallationFlagKey = "MCPForUnity.ServerInstalled";
-
- static PackageInstaller()
- {
- // Check if this is the first time the package is loaded
- if (!EditorPrefs.GetBool(InstallationFlagKey, false))
- {
- // Schedule the installation for after Unity is fully loaded
- EditorApplication.delayCall += InstallServerOnFirstLoad;
- }
- }
-
- private static void InstallServerOnFirstLoad()
- {
- try
- {
- ServerInstaller.EnsureServerInstalled();
-
- // Mark as installed/checked
- EditorPrefs.SetBool(InstallationFlagKey, true);
-
- // Only log success if server was actually embedded and copied
- if (ServerInstaller.HasEmbeddedServer())
- {
- McpLog.Info("MCP server installation completed successfully.");
- }
- }
- catch (System.Exception)
- {
- EditorPrefs.SetBool(InstallationFlagKey, true); // Mark as handled
- McpLog.Info("Server installation pending. Open Window > MCP For Unity to download the server.");
- }
- }
- }
-}
diff --git a/MCPForUnity/Editor/Helpers/PackageLifecycleManager.cs b/MCPForUnity/Editor/Helpers/PackageLifecycleManager.cs
new file mode 100644
index 00000000..02e482c2
--- /dev/null
+++ b/MCPForUnity/Editor/Helpers/PackageLifecycleManager.cs
@@ -0,0 +1,240 @@
+using System.IO;
+using UnityEditor;
+using UnityEngine;
+
+namespace MCPForUnity.Editor.Helpers
+{
+ ///
+ /// Manages package lifecycle events including first-time installation,
+ /// version updates, and legacy installation detection.
+ /// Consolidates the functionality of PackageInstaller and PackageDetector.
+ ///
+ [InitializeOnLoad]
+ public static class PackageLifecycleManager
+ {
+ private const string VersionKeyPrefix = "MCPForUnity.InstalledVersion:";
+ private const string LegacyInstallFlagKey = "MCPForUnity.ServerInstalled"; // For migration
+ private const string InstallErrorKeyPrefix = "MCPForUnity.InstallError:"; // Stores last installation error
+
+ static PackageLifecycleManager()
+ {
+ // Schedule the check for after Unity is fully loaded
+ EditorApplication.delayCall += CheckAndInstallServer;
+ }
+
+ private static void CheckAndInstallServer()
+ {
+ try
+ {
+ string currentVersion = GetPackageVersion();
+ string versionKey = VersionKeyPrefix + currentVersion;
+ bool hasRunForThisVersion = EditorPrefs.GetBool(versionKey, false);
+
+ // Check for conditions that require installation/verification
+ bool isFirstTimeInstall = !EditorPrefs.HasKey(LegacyInstallFlagKey) && !hasRunForThisVersion;
+ bool legacyPresent = LegacyRootsExist();
+ bool canonicalMissing = !File.Exists(
+ Path.Combine(ServerInstaller.GetServerPath(), "server.py")
+ );
+
+ // Run if: first install, version update, legacy detected, or canonical missing
+ if (isFirstTimeInstall || !hasRunForThisVersion || legacyPresent || canonicalMissing)
+ {
+ PerformInstallation(currentVersion, versionKey, isFirstTimeInstall);
+ }
+ }
+ catch (System.Exception ex)
+ {
+ McpLog.Info($"Package lifecycle check failed: {ex.Message}. Open Window > MCP For Unity if needed.", always: false);
+ }
+ }
+
+ private static void PerformInstallation(string version, string versionKey, bool isFirstTimeInstall)
+ {
+ string error = null;
+
+ try
+ {
+ ServerInstaller.EnsureServerInstalled();
+
+ // Mark as installed for this version
+ EditorPrefs.SetBool(versionKey, true);
+
+ // Migrate legacy flag if this is first time
+ if (isFirstTimeInstall)
+ {
+ EditorPrefs.SetBool(LegacyInstallFlagKey, true);
+ }
+
+ // Clean up old version keys (keep only current version)
+ CleanupOldVersionKeys(version);
+
+ // Clean up legacy preference keys
+ CleanupLegacyPrefs();
+
+ // Only log success if server was actually embedded and copied
+ if (ServerInstaller.HasEmbeddedServer() && isFirstTimeInstall)
+ {
+ McpLog.Info("MCP server installation completed successfully.");
+ }
+ }
+ catch (System.Exception ex)
+ {
+ error = ex.Message;
+
+ // Store the error for display in the UI, but don't mark as handled
+ // This allows the user to manually rebuild via the "Rebuild Server" button
+ string errorKey = InstallErrorKeyPrefix + version;
+ EditorPrefs.SetString(errorKey, ex.Message ?? "Unknown error");
+
+ // Don't mark as installed - user needs to manually rebuild
+ }
+
+ if (!string.IsNullOrEmpty(error))
+ {
+ McpLog.Info($"Server installation failed: {error}. Use Window > MCP For Unity > Rebuild Server to retry.", always: false);
+ }
+ }
+
+ private static string GetPackageVersion()
+ {
+ try
+ {
+ var info = UnityEditor.PackageManager.PackageInfo.FindForAssembly(
+ typeof(PackageLifecycleManager).Assembly
+ );
+ if (info != null && !string.IsNullOrEmpty(info.version))
+ {
+ return info.version;
+ }
+ }
+ catch { }
+
+ // Fallback to embedded server version
+ return GetEmbeddedServerVersion();
+ }
+
+ private static string GetEmbeddedServerVersion()
+ {
+ try
+ {
+ if (ServerPathResolver.TryFindEmbeddedServerSource(out var embeddedSrc))
+ {
+ var versionPath = Path.Combine(embeddedSrc, "server_version.txt");
+ if (File.Exists(versionPath))
+ {
+ return File.ReadAllText(versionPath)?.Trim() ?? "unknown";
+ }
+ }
+ }
+ catch { }
+ return "unknown";
+ }
+
+ private static bool LegacyRootsExist()
+ {
+ try
+ {
+ string home = System.Environment.GetFolderPath(
+ System.Environment.SpecialFolder.UserProfile
+ ) ?? string.Empty;
+
+ string[] legacyRoots =
+ {
+ Path.Combine(home, ".config", "UnityMCP", "UnityMcpServer", "src"),
+ Path.Combine(home, ".local", "share", "UnityMCP", "UnityMcpServer", "src")
+ };
+
+ foreach (var root in legacyRoots)
+ {
+ try
+ {
+ if (File.Exists(Path.Combine(root, "server.py")))
+ {
+ return true;
+ }
+ }
+ catch { }
+ }
+ }
+ catch { }
+ return false;
+ }
+
+ private static void CleanupOldVersionKeys(string currentVersion)
+ {
+ try
+ {
+ // Get all EditorPrefs keys that start with our version prefix
+ // Note: Unity doesn't provide a way to enumerate all keys, so we can only
+ // clean up known legacy keys. Future versions will be cleaned up when
+ // a newer version runs.
+ // This is a best-effort cleanup.
+ }
+ catch { }
+ }
+
+ private static void CleanupLegacyPrefs()
+ {
+ try
+ {
+ // Clean up old preference keys that are no longer used
+ string[] legacyKeys =
+ {
+ "MCPForUnity.ServerSrc",
+ "MCPForUnity.PythonDirOverride",
+ "MCPForUnity.LegacyDetectLogged" // Old prefix without version
+ };
+
+ foreach (var key in legacyKeys)
+ {
+ try
+ {
+ if (EditorPrefs.HasKey(key))
+ {
+ EditorPrefs.DeleteKey(key);
+ }
+ }
+ catch { }
+ }
+ }
+ catch { }
+ }
+
+ ///
+ /// Gets the last installation error for the current package version, if any.
+ /// Returns null if there was no error or the error has been cleared.
+ ///
+ public static string GetLastInstallError()
+ {
+ try
+ {
+ string currentVersion = GetPackageVersion();
+ string errorKey = InstallErrorKeyPrefix + currentVersion;
+ if (EditorPrefs.HasKey(errorKey))
+ {
+ return EditorPrefs.GetString(errorKey, null);
+ }
+ }
+ catch { }
+ return null;
+ }
+
+ ///
+ /// Clears the last installation error. Should be called after a successful manual rebuild.
+ ///
+ public static void ClearLastInstallError()
+ {
+ try
+ {
+ string currentVersion = GetPackageVersion();
+ string errorKey = InstallErrorKeyPrefix + currentVersion;
+ if (EditorPrefs.HasKey(errorKey))
+ {
+ EditorPrefs.DeleteKey(errorKey);
+ }
+ }
+ catch { }
+ }
+ }
+}
diff --git a/MCPForUnity/Editor/Helpers/PackageDetector.cs.meta b/MCPForUnity/Editor/Helpers/PackageLifecycleManager.cs.meta
similarity index 83%
rename from MCPForUnity/Editor/Helpers/PackageDetector.cs.meta
rename to MCPForUnity/Editor/Helpers/PackageLifecycleManager.cs.meta
index f1a5dbe4..f1e14f70 100644
--- a/MCPForUnity/Editor/Helpers/PackageDetector.cs.meta
+++ b/MCPForUnity/Editor/Helpers/PackageLifecycleManager.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: b82eaef548d164ca095f17db64d15af8
+guid: c40bd28f2310d463c8cd00181202cbe4
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs b/MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs
index fce0e783..de6167a7 100644
--- a/MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs
+++ b/MCPForUnity/Editor/Helpers/PythonToolSyncProcessor.cs
@@ -139,9 +139,8 @@ public static void SetAutoSyncEnabled(bool enabled)
}
///
- /// Menu item to reimport all Python files in the project
+ /// Reimport all Python files in the project
///
- [MenuItem("Window/MCP For Unity/Tool Sync/Reimport Python Files", priority = 99)]
public static void ReimportPythonFiles()
{
// Find all Python files (imported as TextAssets by PythonFileImporter)
@@ -161,9 +160,8 @@ public static void ReimportPythonFiles()
}
///
- /// Menu item to manually trigger sync
+ /// Manually trigger sync
///
- [MenuItem("Window/MCP For Unity/Tool Sync/Sync Python Tools", priority = 100)]
public static void ManualSync()
{
McpLog.Info("Manually syncing Python tools...");
@@ -171,9 +169,8 @@ public static void ManualSync()
}
///
- /// Menu item to toggle auto-sync
+ /// Toggle auto-sync
///
- [MenuItem("Window/MCP For Unity/Tool Sync/Auto-Sync Python Tools", priority = 101)]
public static void ToggleAutoSync()
{
SetAutoSyncEnabled(!IsAutoSyncEnabled());
@@ -182,7 +179,6 @@ public static void ToggleAutoSync()
///
/// Validate menu item (shows checkmark when enabled)
///
- [MenuItem("Window/MCP For Unity/Tool Sync/Auto-Sync Python Tools", true, priority = 101)]
public static bool ToggleAutoSyncValidate()
{
Menu.SetChecked("Window/MCP For Unity/Tool Sync/Auto-Sync Python Tools", IsAutoSyncEnabled());
diff --git a/MCPForUnity/Editor/Helpers/ServerInstaller.cs b/MCPForUnity/Editor/Helpers/ServerInstaller.cs
index 2b0c8f45..10666342 100644
--- a/MCPForUnity/Editor/Helpers/ServerInstaller.cs
+++ b/MCPForUnity/Editor/Helpers/ServerInstaller.cs
@@ -84,14 +84,13 @@ public static void EnsureServerInstalled()
if (legacyOlder)
{
TryKillUvForPath(legacySrc);
- try
+ if (DeleteDirectoryWithRetry(legacyRoot))
{
- Directory.Delete(legacyRoot, recursive: true);
McpLog.Info($"Removed legacy server at '{legacyRoot}'.");
}
- catch (Exception ex)
+ else
{
- McpLog.Warn($"Failed to remove legacy server at '{legacyRoot}': {ex.Message}");
+ McpLog.Warn($"Failed to remove legacy server at '{legacyRoot}' (files may be in use)");
}
}
}
@@ -338,13 +337,24 @@ private static IEnumerable GetLegacyRootsForDetection()
return roots;
}
+ ///
+ /// Attempts to kill UV and Python processes associated with a specific server path.
+ /// This is necessary on Windows because the OS blocks file deletion when processes
+ /// have open file handles, unlike macOS/Linux which allow unlinking open files.
+ ///
private static void TryKillUvForPath(string serverSrcPath)
{
try
{
if (string.IsNullOrEmpty(serverSrcPath)) return;
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return;
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ KillWindowsUvProcesses(serverSrcPath);
+ return;
+ }
+
+ // Unix: use pgrep to find processes by command line
var psi = new ProcessStartInfo
{
FileName = "/usr/bin/pgrep",
@@ -372,6 +382,148 @@ private static void TryKillUvForPath(string serverSrcPath)
catch { }
}
+ ///
+ /// Kills Windows processes running from the virtual environment directory.
+ /// Uses WMIC (Windows Management Instrumentation) to safely query only processes
+ /// with executables in the .venv path, avoiding the need to iterate all system processes.
+ /// This prevents accidentally killing IDE processes or other critical system processes.
+ ///
+ /// Why this is needed on Windows:
+ /// - Windows blocks file/directory deletion when ANY process has an open file handle
+ /// - UV creates a virtual environment with python.exe and other executables
+ /// - These processes may hold locks on DLLs, .pyd files, or the executables themselves
+ /// - macOS/Linux allow deletion of open files (unlink), but Windows does not
+ ///
+ private static void KillWindowsUvProcesses(string serverSrcPath)
+ {
+ try
+ {
+ if (string.IsNullOrEmpty(serverSrcPath)) return;
+
+ string venvPath = Path.Combine(serverSrcPath, ".venv");
+ if (!Directory.Exists(venvPath)) return;
+
+ string normalizedVenvPath = Path.GetFullPath(venvPath).ToLowerInvariant();
+
+ // Use WMIC to find processes with executables in the .venv directory
+ // This is much safer than iterating all processes
+ var psi = new ProcessStartInfo
+ {
+ FileName = "wmic",
+ Arguments = $"process where \"ExecutablePath like '%{normalizedVenvPath.Replace("\\", "\\\\")}%'\" get ProcessId",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true
+ };
+
+ using var proc = Process.Start(psi);
+ if (proc == null) return;
+
+ string output = proc.StandardOutput.ReadToEnd();
+ proc.WaitForExit(5000);
+
+ if (proc.ExitCode != 0) return;
+
+ // Parse PIDs from WMIC output
+ var lines = output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
+ foreach (var line in lines)
+ {
+ string trimmed = line.Trim();
+ if (trimmed.Equals("ProcessId", StringComparison.OrdinalIgnoreCase)) continue;
+ if (string.IsNullOrWhiteSpace(trimmed)) continue;
+
+ if (int.TryParse(trimmed, out int pid))
+ {
+ try
+ {
+ using var p = Process.GetProcessById(pid);
+ // Double-check it's not a critical process
+ string name = p.ProcessName.ToLowerInvariant();
+ if (name == "unity" || name == "code" || name == "devenv" || name == "rider64")
+ {
+ continue; // Skip IDE processes
+ }
+ p.Kill();
+ p.WaitForExit(2000);
+ }
+ catch { }
+ }
+ }
+
+ // Give processes time to fully exit
+ System.Threading.Thread.Sleep(500);
+ }
+ catch { }
+ }
+
+ ///
+ /// Attempts to delete a directory with retry logic to handle Windows file locking issues.
+ ///
+ /// Why retries are necessary on Windows:
+ /// - Even after killing processes, Windows may take time to release file handles
+ /// - Antivirus, Windows Defender, or indexing services may temporarily lock files
+ /// - File Explorer previews can hold locks on certain file types
+ /// - Readonly attributes on files (common in .venv) block deletion
+ ///
+ /// This method handles these cases by:
+ /// - Retrying deletion after a delay to allow handle release
+ /// - Clearing readonly attributes that block deletion
+ /// - Distinguishing between temporary locks (retry) and permanent failures
+ ///
+ private static bool DeleteDirectoryWithRetry(string path, int maxRetries = 3, int delayMs = 500)
+ {
+ for (int i = 0; i < maxRetries; i++)
+ {
+ try
+ {
+ if (!Directory.Exists(path)) return true;
+
+ Directory.Delete(path, recursive: true);
+ return true;
+ }
+ catch (UnauthorizedAccessException)
+ {
+ if (i < maxRetries - 1)
+ {
+ // Wait for file handles to be released
+ System.Threading.Thread.Sleep(delayMs);
+
+ // Try to clear readonly attributes
+ try
+ {
+ foreach (var file in Directory.GetFiles(path, "*", SearchOption.AllDirectories))
+ {
+ try
+ {
+ var attrs = File.GetAttributes(file);
+ if ((attrs & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
+ {
+ File.SetAttributes(file, attrs & ~FileAttributes.ReadOnly);
+ }
+ }
+ catch { }
+ }
+ }
+ catch { }
+ }
+ }
+ catch (IOException)
+ {
+ if (i < maxRetries - 1)
+ {
+ // File in use, wait and retry
+ System.Threading.Thread.Sleep(delayMs);
+ }
+ }
+ catch
+ {
+ return false;
+ }
+ }
+ return false;
+ }
+
private static string ReadVersionFile(string path)
{
try
@@ -459,16 +611,12 @@ public static bool RebuildMcpServer()
// Delete the entire installed server directory
if (Directory.Exists(destRoot))
{
- try
+ if (!DeleteDirectoryWithRetry(destRoot, maxRetries: 5, delayMs: 1000))
{
- Directory.Delete(destRoot, recursive: true);
- McpLog.Info($"Deleted existing server at {destRoot}");
- }
- catch (Exception ex)
- {
- McpLog.Error($"Failed to delete existing server: {ex.Message}");
+ McpLog.Error($"Failed to delete existing server at {destRoot}. Please close any applications using the Python virtual environment and try again.");
return false;
}
+ McpLog.Info($"Deleted existing server at {destRoot}");
}
// Re-copy from embedded source
@@ -488,6 +636,12 @@ public static bool RebuildMcpServer()
}
McpLog.Info($"Server rebuilt successfully at {destRoot} (version {embeddedVer})");
+
+ // Clear any previous installation error
+
+ PackageLifecycleManager.ClearLastInstallError();
+
+
return true;
}
catch (Exception ex)
@@ -747,13 +901,9 @@ public static bool DownloadAndInstallServer()
// Delete old installation
if (Directory.Exists(destRoot))
{
- try
- {
- Directory.Delete(destRoot, recursive: true);
- }
- catch (Exception ex)
+ if (!DeleteDirectoryWithRetry(destRoot, maxRetries: 5, delayMs: 1000))
{
- McpLog.Warn($"Could not fully delete old server: {ex.Message}");
+ McpLog.Warn($"Could not fully delete old server (files may be in use)");
}
}
@@ -803,9 +953,12 @@ public static bool DownloadAndInstallServer()
}
finally
{
- try {
- if (File.Exists(tempZip)) File.Delete(tempZip);
- } catch (Exception ex) {
+ try
+ {
+ if (File.Exists(tempZip)) File.Delete(tempZip);
+ }
+ catch (Exception ex)
+ {
McpLog.Warn($"Could not delete temp zip file: {ex.Message}");
}
}
diff --git a/MCPForUnity/Editor/Helpers/Vector3Helper.cs b/MCPForUnity/Editor/Helpers/Vector3Helper.cs
deleted file mode 100644
index 41566188..00000000
--- a/MCPForUnity/Editor/Helpers/Vector3Helper.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using Newtonsoft.Json.Linq;
-using UnityEngine;
-
-namespace MCPForUnity.Editor.Helpers
-{
- ///
- /// Helper class for Vector3 operations
- ///
- public static class Vector3Helper
- {
- ///
- /// Parses a JArray into a Vector3
- ///
- /// The array containing x, y, z coordinates
- /// A Vector3 with the parsed coordinates
- /// Thrown when array is invalid
- public static Vector3 ParseVector3(JArray array)
- {
- if (array == null || array.Count != 3)
- throw new System.Exception("Vector3 must be an array of 3 floats [x, y, z].");
- return new Vector3((float)array[0], (float)array[1], (float)array[2]);
- }
- }
-}
diff --git a/MCPForUnity/Editor/Helpers/Vector3Helper.cs.meta b/MCPForUnity/Editor/Helpers/Vector3Helper.cs.meta
deleted file mode 100644
index 280381ca..00000000
--- a/MCPForUnity/Editor/Helpers/Vector3Helper.cs.meta
+++ /dev/null
@@ -1,11 +0,0 @@
-fileFormatVersion: 2
-guid: f8514fd42f23cb641a36e52550825b35
-MonoImporter:
- externalObjects: {}
- serializedVersion: 2
- defaultReferences: []
- executionOrder: 0
- icon: {instanceID: 0}
- userData:
- assetBundleName:
- assetBundleVariant:
diff --git a/MCPForUnity/Editor/MCPForUnityMenu.cs b/MCPForUnity/Editor/MCPForUnityMenu.cs
new file mode 100644
index 00000000..714e4853
--- /dev/null
+++ b/MCPForUnity/Editor/MCPForUnityMenu.cs
@@ -0,0 +1,75 @@
+using MCPForUnity.Editor.Helpers;
+using MCPForUnity.Editor.Setup;
+using MCPForUnity.Editor.Windows;
+using UnityEditor;
+
+namespace MCPForUnity.Editor
+{
+ ///
+ /// Centralized menu items for MCP For Unity
+ ///
+ public static class MCPForUnityMenu
+ {
+ // ========================================
+ // Main Menu Items
+ // ========================================
+
+ ///
+ /// Show the setup wizard
+ ///
+ [MenuItem("Window/MCP For Unity/Setup Wizard", priority = 1)]
+ public static void ShowSetupWizard()
+ {
+ SetupWizard.ShowSetupWizard();
+ }
+
+ ///
+ /// Open the main MCP For Unity window
+ ///
+ [MenuItem("Window/MCP For Unity/Open MCP Window %#m", priority = 2)]
+ public static void OpenMCPWindow()
+ {
+ MCPForUnityEditorWindow.ShowWindow();
+ }
+
+ // ========================================
+ // Tool Sync Menu Items
+ // ========================================
+
+ ///
+ /// Reimport all Python files in the project
+ ///
+ [MenuItem("Window/MCP For Unity/Tool Sync/Reimport Python Files", priority = 99)]
+ public static void ReimportPythonFiles()
+ {
+ PythonToolSyncProcessor.ReimportPythonFiles();
+ }
+
+ ///
+ /// Manually sync Python tools to the MCP server
+ ///
+ [MenuItem("Window/MCP For Unity/Tool Sync/Sync Python Tools", priority = 100)]
+ public static void SyncPythonTools()
+ {
+ PythonToolSyncProcessor.ManualSync();
+ }
+
+ ///
+ /// Toggle auto-sync for Python tools
+ ///
+ [MenuItem("Window/MCP For Unity/Tool Sync/Auto-Sync Python Tools", priority = 101)]
+ public static void ToggleAutoSync()
+ {
+ PythonToolSyncProcessor.ToggleAutoSync();
+ }
+
+ ///
+ /// Validate menu item (shows checkmark when auto-sync is enabled)
+ ///
+ [MenuItem("Window/MCP For Unity/Tool Sync/Auto-Sync Python Tools", true, priority = 101)]
+ public static bool ToggleAutoSyncValidate()
+ {
+ return PythonToolSyncProcessor.ToggleAutoSyncValidate();
+ }
+ }
+}
diff --git a/MCPForUnity/Editor/Data/DefaultServerConfig.cs.meta b/MCPForUnity/Editor/MCPForUnityMenu.cs.meta
similarity index 83%
rename from MCPForUnity/Editor/Data/DefaultServerConfig.cs.meta
rename to MCPForUnity/Editor/MCPForUnityMenu.cs.meta
index 82e437f2..af82a270 100644
--- a/MCPForUnity/Editor/Data/DefaultServerConfig.cs.meta
+++ b/MCPForUnity/Editor/MCPForUnityMenu.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: de8f5721c34f7194392e9d8c7d0226c0
+guid: 42b27c415aa084fe6a9cc6cf03979d36
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/MCPForUnity/Editor/Models/ServerConfig.cs b/MCPForUnity/Editor/Models/ServerConfig.cs
deleted file mode 100644
index 4b185f1f..00000000
--- a/MCPForUnity/Editor/Models/ServerConfig.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using System;
-using Newtonsoft.Json;
-
-namespace MCPForUnity.Editor.Models
-{
- [Serializable]
- public class ServerConfig
- {
- [JsonProperty("unity_host")]
- public string unityHost = "localhost";
-
- [JsonProperty("unity_port")]
- public int unityPort;
-
- [JsonProperty("mcp_port")]
- public int mcpPort;
-
- [JsonProperty("connection_timeout")]
- public float connectionTimeout;
-
- [JsonProperty("buffer_size")]
- public int bufferSize;
-
- [JsonProperty("log_level")]
- public string logLevel;
-
- [JsonProperty("log_format")]
- public string logFormat;
-
- [JsonProperty("max_retries")]
- public int maxRetries;
-
- [JsonProperty("retry_delay")]
- public float retryDelay;
- }
-}
diff --git a/MCPForUnity/Editor/Models/ServerConfig.cs.meta b/MCPForUnity/Editor/Models/ServerConfig.cs.meta
deleted file mode 100644
index 6e675e9e..00000000
--- a/MCPForUnity/Editor/Models/ServerConfig.cs.meta
+++ /dev/null
@@ -1,11 +0,0 @@
-fileFormatVersion: 2
-guid: e4e45386fcc282249907c2e3c7e5d9c6
-MonoImporter:
- externalObjects: {}
- serializedVersion: 2
- defaultReferences: []
- executionOrder: 0
- icon: {instanceID: 0}
- userData:
- assetBundleName:
- assetBundleVariant:
diff --git a/MCPForUnity/Editor/Services/ClientConfigurationService.cs b/MCPForUnity/Editor/Services/ClientConfigurationService.cs
index dea5358a..8a9c4caf 100644
--- a/MCPForUnity/Editor/Services/ClientConfigurationService.cs
+++ b/MCPForUnity/Editor/Services/ClientConfigurationService.cs
@@ -176,9 +176,9 @@ public bool CheckClientStatus(McpClient client, bool attemptAutoRewrite = true)
if (configExists)
{
- string configuredDir = McpConfigFileHelper.ExtractDirectoryArg(args);
+ string configuredDir = McpConfigurationHelper.ExtractDirectoryArg(args);
bool matches = !string.IsNullOrEmpty(configuredDir) &&
- McpConfigFileHelper.PathsEqual(configuredDir, pythonDir);
+ McpConfigurationHelper.PathsEqual(configuredDir, pythonDir);
if (matches)
{
@@ -396,7 +396,7 @@ public string GenerateConfigJson(McpClient client)
if (client.mcpType == McpTypes.Codex)
{
return CodexConfigHelper.BuildCodexServerBlock(uvPath,
- McpConfigFileHelper.ResolveServerDirectory(pythonDir, null));
+ McpConfigurationHelper.ResolveServerDirectory(pythonDir, null));
}
else
{
diff --git a/MCPForUnity/Editor/Services/IPlatformService.cs b/MCPForUnity/Editor/Services/IPlatformService.cs
new file mode 100644
index 00000000..ec686b24
--- /dev/null
+++ b/MCPForUnity/Editor/Services/IPlatformService.cs
@@ -0,0 +1,20 @@
+namespace MCPForUnity.Editor.Services
+{
+ ///
+ /// Service for platform detection and platform-specific environment access
+ ///
+ public interface IPlatformService
+ {
+ ///
+ /// Checks if the current platform is Windows
+ ///
+ /// True if running on Windows
+ bool IsWindows();
+
+ ///
+ /// Gets the SystemRoot environment variable (Windows-specific)
+ ///
+ /// SystemRoot path, or null if not available
+ string GetSystemRoot();
+ }
+}
diff --git a/MCPForUnity/Editor/Helpers/PackageInstaller.cs.meta b/MCPForUnity/Editor/Services/IPlatformService.cs.meta
similarity index 83%
rename from MCPForUnity/Editor/Helpers/PackageInstaller.cs.meta
rename to MCPForUnity/Editor/Services/IPlatformService.cs.meta
index 156e75fb..e501f58a 100644
--- a/MCPForUnity/Editor/Helpers/PackageInstaller.cs.meta
+++ b/MCPForUnity/Editor/Services/IPlatformService.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: 19e6eaa637484e9fa19f9a0459809de2
+guid: 1d90ff7f9a1e84c9bbbbedee2f7eda2a
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/MCPForUnity/Editor/Services/MCPServiceLocator.cs b/MCPForUnity/Editor/Services/MCPServiceLocator.cs
index 2a7f070c..a743d4ce 100644
--- a/MCPForUnity/Editor/Services/MCPServiceLocator.cs
+++ b/MCPForUnity/Editor/Services/MCPServiceLocator.cs
@@ -14,6 +14,7 @@ public static class MCPServiceLocator
private static ITestRunnerService _testRunnerService;
private static IToolSyncService _toolSyncService;
private static IPackageUpdateService _packageUpdateService;
+ private static IPlatformService _platformService;
public static IBridgeControlService Bridge => _bridgeService ??= new BridgeControlService();
public static IClientConfigurationService Client => _clientService ??= new ClientConfigurationService();
@@ -22,6 +23,7 @@ public static class MCPServiceLocator
public static ITestRunnerService Tests => _testRunnerService ??= new TestRunnerService();
public static IToolSyncService ToolSync => _toolSyncService ??= new ToolSyncService();
public static IPackageUpdateService Updates => _packageUpdateService ??= new PackageUpdateService();
+ public static IPlatformService Platform => _platformService ??= new PlatformService();
///
/// Registers a custom implementation for a service (useful for testing)
@@ -44,6 +46,8 @@ public static void Register(T implementation) where T : class
_toolSyncService = ts;
else if (implementation is IPackageUpdateService pu)
_packageUpdateService = pu;
+ else if (implementation is IPlatformService ps)
+ _platformService = ps;
}
///
@@ -58,6 +62,7 @@ public static void Reset()
(_testRunnerService as IDisposable)?.Dispose();
(_toolSyncService as IDisposable)?.Dispose();
(_packageUpdateService as IDisposable)?.Dispose();
+ (_platformService as IDisposable)?.Dispose();
_bridgeService = null;
_clientService = null;
@@ -66,6 +71,7 @@ public static void Reset()
_testRunnerService = null;
_toolSyncService = null;
_packageUpdateService = null;
+ _platformService = null;
}
}
}
diff --git a/MCPForUnity/Editor/Services/PlatformService.cs b/MCPForUnity/Editor/Services/PlatformService.cs
new file mode 100644
index 00000000..6e663717
--- /dev/null
+++ b/MCPForUnity/Editor/Services/PlatformService.cs
@@ -0,0 +1,31 @@
+using System;
+
+namespace MCPForUnity.Editor.Services
+{
+ ///
+ /// Default implementation of platform detection service
+ ///
+ public class PlatformService : IPlatformService
+ {
+ ///
+ /// Checks if the current platform is Windows
+ ///
+ /// True if running on Windows
+ public bool IsWindows()
+ {
+ return Environment.OSVersion.Platform == PlatformID.Win32NT;
+ }
+
+ ///
+ /// Gets the SystemRoot environment variable (Windows-specific)
+ ///
+ /// SystemRoot path, or "C:\\Windows" as fallback on Windows, null on other platforms
+ public string GetSystemRoot()
+ {
+ if (!IsWindows())
+ return null;
+
+ return Environment.GetEnvironmentVariable("SystemRoot") ?? "C:\\Windows";
+ }
+ }
+}
diff --git a/MCPForUnity/Editor/Helpers/McpConfigFileHelper.cs.meta b/MCPForUnity/Editor/Services/PlatformService.cs.meta
similarity index 83%
rename from MCPForUnity/Editor/Helpers/McpConfigFileHelper.cs.meta
rename to MCPForUnity/Editor/Services/PlatformService.cs.meta
index 8f81ae99..172daf8f 100644
--- a/MCPForUnity/Editor/Helpers/McpConfigFileHelper.cs.meta
+++ b/MCPForUnity/Editor/Services/PlatformService.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: f69ad468942b74c0ea24e3e8e5f21a4b
+guid: 3b2d7f32a595c45dd8c01f141c69761c
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/MCPForUnity/Editor/Setup/SetupWizard.cs b/MCPForUnity/Editor/Setup/SetupWizard.cs
index 691e482f..7bb77ded 100644
--- a/MCPForUnity/Editor/Setup/SetupWizard.cs
+++ b/MCPForUnity/Editor/Setup/SetupWizard.cs
@@ -97,63 +97,5 @@ public static void MarkSetupDismissed()
McpLog.Info("Setup marked as dismissed");
}
- ///
- /// Force show setup wizard (for manual invocation)
- ///
- [MenuItem("Window/MCP For Unity/Setup Wizard", priority = 1)]
- public static void ShowSetupWizardManual()
- {
- ShowSetupWizard();
- }
-
- ///
- /// Check dependencies and show status
- ///
- [MenuItem("Window/MCP For Unity/Check Dependencies", priority = 3)]
- public static void CheckDependencies()
- {
- var result = DependencyManager.CheckAllDependencies();
-
- if (!result.IsSystemReady)
- {
- bool showWizard = EditorUtility.DisplayDialog(
- "MCP for Unity - Dependencies",
- $"System Status: {result.Summary}\n\nWould you like to open the Setup Wizard?",
- "Open Setup Wizard",
- "Close"
- );
-
- if (showWizard)
- {
- ShowSetupWizard(result);
- }
- }
- else
- {
- EditorUtility.DisplayDialog(
- "MCP for Unity - Dependencies",
- "✓ All dependencies are available and ready!\n\nMCP for Unity is ready to use.",
- "OK"
- );
- }
- }
-
- ///
- /// Open MCP Client Configuration window
- ///
- [MenuItem("Window/MCP For Unity/Open MCP Window %#m", priority = 4)]
- public static void OpenClientConfiguration()
- {
- Windows.MCPForUnityEditorWindowNew.ShowWindow();
- }
-
- ///
- /// Open legacy MCP Client Configuration window
- ///
- [MenuItem("Window/MCP For Unity/Open Legacy MCP Window", priority = 5)]
- public static void OpenLegacyClientConfiguration()
- {
- Windows.MCPForUnityEditorWindow.ShowWindow();
- }
}
}
diff --git a/MCPForUnity/Editor/Setup/SetupWizardWindow.cs b/MCPForUnity/Editor/Setup/SetupWizardWindow.cs
index 7229be97..40599c8e 100644
--- a/MCPForUnity/Editor/Setup/SetupWizardWindow.cs
+++ b/MCPForUnity/Editor/Setup/SetupWizardWindow.cs
@@ -18,12 +18,9 @@ public class SetupWizardWindow : EditorWindow
private DependencyCheckResult _dependencyResult;
private Vector2 _scrollPosition;
private int _currentStep = 0;
- private McpClients _mcpClients;
- private int _selectedClientIndex = 0;
private readonly string[] _stepTitles = {
"Setup",
- "Configure",
"Complete"
};
@@ -42,14 +39,6 @@ private void OnEnable()
{
_dependencyResult = DependencyManager.CheckAllDependencies();
}
-
- _mcpClients = new McpClients();
-
- // Check client configurations on startup
- foreach (var client in _mcpClients.clients)
- {
- CheckClientConfiguration(client);
- }
}
private void OnGUI()
@@ -62,8 +51,7 @@ private void OnGUI()
switch (_currentStep)
{
case 0: DrawSetupStep(); break;
- case 1: DrawConfigureStep(); break;
- case 2: DrawCompleteStep(); break;
+ case 1: DrawCompleteStep(); break;
}
EditorGUILayout.EndScrollView();
@@ -132,7 +120,7 @@ private void DrawSetupStep()
{
// Only show critical warnings when dependencies are actually missing
EditorGUILayout.HelpBox(
- "⚠️ Missing Dependencies: MCP for Unity requires Python 3.10+ and UV package manager to function properly.",
+ "\u26A0 Missing Dependencies: MCP for Unity requires Python 3.11+ and UV package manager to function properly.",
MessageType.Warning
);
@@ -157,8 +145,6 @@ private void DrawSetupStep()
}
}
-
-
private void DrawCompleteStep()
{
DrawSectionTitle("Setup Complete");
@@ -273,85 +259,6 @@ private void DrawSimpleDependencyStatus(DependencyStatus dep)
EditorGUILayout.EndHorizontal();
}
- private void DrawConfigureStep()
- {
- DrawSectionTitle("AI Client Configuration");
-
- // Check dependencies first (with caching to avoid heavy operations on every repaint)
- if (_dependencyResult == null || (DateTime.UtcNow - _dependencyResult.CheckedAt).TotalSeconds > 2)
- {
- _dependencyResult = DependencyManager.CheckAllDependencies();
- }
- if (!_dependencyResult.IsSystemReady)
- {
- DrawErrorStatus("Cannot Configure - System Requirements Not Met");
-
- EditorGUILayout.HelpBox(
- "Client configuration requires system dependencies to be installed first. Please complete setup before proceeding.",
- MessageType.Warning
- );
-
- if (GUILayout.Button("Go Back to Setup", GUILayout.Height(30)))
- {
- _currentStep = 0;
- }
- return;
- }
-
- EditorGUILayout.LabelField(
- "Configure your AI assistants to work with Unity. Select a client below to set it up:",
- EditorStyles.wordWrappedLabel
- );
- EditorGUILayout.Space();
-
- // Client selection and configuration
- if (_mcpClients.clients.Count > 0)
- {
- // Client selector dropdown
- string[] clientNames = _mcpClients.clients.Select(c => c.name).ToArray();
- EditorGUI.BeginChangeCheck();
- _selectedClientIndex = EditorGUILayout.Popup("Select AI Client:", _selectedClientIndex, clientNames);
- if (EditorGUI.EndChangeCheck())
- {
- _selectedClientIndex = Mathf.Clamp(_selectedClientIndex, 0, _mcpClients.clients.Count - 1);
- // Refresh client status when selection changes
- CheckClientConfiguration(_mcpClients.clients[_selectedClientIndex]);
- }
-
- EditorGUILayout.Space();
-
- var selectedClient = _mcpClients.clients[_selectedClientIndex];
- DrawClientConfigurationInWizard(selectedClient);
-
- EditorGUILayout.Space();
-
- // Batch configuration option
- EditorGUILayout.BeginVertical(EditorStyles.helpBox);
- EditorGUILayout.LabelField("Quick Setup", EditorStyles.boldLabel);
- EditorGUILayout.LabelField(
- "Automatically configure all detected AI clients at once:",
- EditorStyles.wordWrappedLabel
- );
- EditorGUILayout.Space();
-
- if (GUILayout.Button("Configure All Detected Clients", GUILayout.Height(30)))
- {
- ConfigureAllClientsInWizard();
- }
- EditorGUILayout.EndVertical();
- }
- else
- {
- EditorGUILayout.HelpBox("No AI clients detected. Make sure you have Claude Code, Cursor, or VSCode installed.", MessageType.Info);
- }
-
- EditorGUILayout.Space();
- EditorGUILayout.HelpBox(
- "💡 You might need to restart your AI client after configuring.",
- MessageType.Info
- );
- }
-
private void DrawFooter()
{
EditorGUILayout.Space();
@@ -371,7 +278,7 @@ private void DrawFooter()
{
bool dismiss = EditorUtility.DisplayDialog(
"Skip Setup",
- "⚠️ Skipping setup will leave MCP for Unity non-functional!\n\n" +
+ "\u26A0 Skipping setup will leave MCP for Unity non-functional!\n\n" +
"You can restart setup from: Window > MCP for Unity > Setup Wizard (Required)",
"Skip Anyway",
"Cancel"
@@ -405,295 +312,6 @@ private void DrawFooter()
EditorGUILayout.EndHorizontal();
}
- private void DrawClientConfigurationInWizard(McpClient client)
- {
- EditorGUILayout.BeginVertical(EditorStyles.helpBox);
-
- EditorGUILayout.LabelField($"{client.name} Configuration", EditorStyles.boldLabel);
- EditorGUILayout.Space();
-
- // Show current status
- var statusColor = GetClientStatusColor(client);
- var originalColor = GUI.color;
- GUI.color = statusColor;
- EditorGUILayout.LabelField($"Status: {client.configStatus}", EditorStyles.label);
- GUI.color = originalColor;
-
- EditorGUILayout.Space();
-
- // Configuration buttons
- EditorGUILayout.BeginHorizontal();
-
- if (client.mcpType == McpTypes.ClaudeCode)
- {
- // Special handling for Claude Code
- bool claudeAvailable = !string.IsNullOrEmpty(ExecPath.ResolveClaude());
- if (claudeAvailable)
- {
- bool isConfigured = client.status == McpStatus.Configured;
- string buttonText = isConfigured ? "Unregister" : "Register";
- if (GUILayout.Button($"{buttonText} with Claude Code"))
- {
- if (isConfigured)
- {
- UnregisterFromClaudeCode(client);
- }
- else
- {
- RegisterWithClaudeCode(client);
- }
- }
- }
- else
- {
- EditorGUILayout.HelpBox("Claude Code not found. Please install Claude Code first.", MessageType.Warning);
- if (GUILayout.Button("Open Claude Code Website"))
- {
- Application.OpenURL("https://claude.ai/download");
- }
- }
- }
- else
- {
- // Standard client configuration
- if (GUILayout.Button($"Configure {client.name}"))
- {
- ConfigureClientInWizard(client);
- }
-
- if (GUILayout.Button("Manual Setup"))
- {
- ShowManualSetupInWizard(client);
- }
- }
-
- EditorGUILayout.EndHorizontal();
- EditorGUILayout.EndVertical();
- }
-
- private Color GetClientStatusColor(McpClient client)
- {
- return client.status switch
- {
- McpStatus.Configured => Color.green,
- McpStatus.Running => Color.green,
- McpStatus.Connected => Color.green,
- McpStatus.IncorrectPath => Color.yellow,
- McpStatus.CommunicationError => Color.yellow,
- McpStatus.NoResponse => Color.yellow,
- _ => Color.red
- };
- }
-
- private void ConfigureClientInWizard(McpClient client)
- {
- try
- {
- string result = PerformClientConfiguration(client);
-
- EditorUtility.DisplayDialog(
- $"{client.name} Configuration",
- result,
- "OK"
- );
-
- // Refresh client status
- CheckClientConfiguration(client);
- Repaint();
- }
- catch (System.Exception ex)
- {
- EditorUtility.DisplayDialog(
- "Configuration Error",
- $"Failed to configure {client.name}: {ex.Message}",
- "OK"
- );
- }
- }
-
- private void ConfigureAllClientsInWizard()
- {
- int successCount = 0;
- int totalCount = _mcpClients.clients.Count;
-
- foreach (var client in _mcpClients.clients)
- {
- try
- {
- if (client.mcpType == McpTypes.ClaudeCode)
- {
- if (!string.IsNullOrEmpty(ExecPath.ResolveClaude()) && client.status != McpStatus.Configured)
- {
- RegisterWithClaudeCode(client);
- successCount++;
- }
- else if (client.status == McpStatus.Configured)
- {
- successCount++; // Already configured
- }
- }
- else
- {
- string result = PerformClientConfiguration(client);
- if (result.Contains("success", System.StringComparison.OrdinalIgnoreCase))
- {
- successCount++;
- }
- }
-
- CheckClientConfiguration(client);
- }
- catch (System.Exception ex)
- {
- McpLog.Error($"Failed to configure {client.name}: {ex.Message}");
- }
- }
-
- EditorUtility.DisplayDialog(
- "Batch Configuration Complete",
- $"Successfully configured {successCount} out of {totalCount} clients.\n\n" +
- "Restart your AI clients for changes to take effect.",
- "OK"
- );
-
- Repaint();
- }
-
- private void RegisterWithClaudeCode(McpClient client)
- {
- try
- {
- string pythonDir = McpPathResolver.FindPackagePythonDirectory();
- string claudePath = ExecPath.ResolveClaude();
- string uvPath = ExecPath.ResolveUv() ?? "uv";
-
- string args = $"mcp add UnityMCP -- \"{uvPath}\" run --directory \"{pythonDir}\" server.py";
-
- if (!ExecPath.TryRun(claudePath, args, null, out var stdout, out var stderr, 15000, McpPathResolver.GetPathPrepend()))
- {
- if ((stdout + stderr).Contains("already exists", System.StringComparison.OrdinalIgnoreCase))
- {
- CheckClientConfiguration(client);
- EditorUtility.DisplayDialog("Claude Code", "MCP for Unity is already registered with Claude Code.", "OK");
- }
- else
- {
- throw new System.Exception($"Registration failed: {stderr}");
- }
- }
- else
- {
- CheckClientConfiguration(client);
- EditorUtility.DisplayDialog("Claude Code", "Successfully registered MCP for Unity with Claude Code!", "OK");
- }
- }
- catch (System.Exception ex)
- {
- EditorUtility.DisplayDialog("Registration Error", $"Failed to register with Claude Code: {ex.Message}", "OK");
- }
- }
-
- private void UnregisterFromClaudeCode(McpClient client)
- {
- try
- {
- string claudePath = ExecPath.ResolveClaude();
- if (ExecPath.TryRun(claudePath, "mcp remove UnityMCP", null, out var stdout, out var stderr, 10000, McpPathResolver.GetPathPrepend()))
- {
- CheckClientConfiguration(client);
- EditorUtility.DisplayDialog("Claude Code", "Successfully unregistered MCP for Unity from Claude Code.", "OK");
- }
- else
- {
- throw new System.Exception($"Unregistration failed: {stderr}");
- }
- }
- catch (System.Exception ex)
- {
- EditorUtility.DisplayDialog("Unregistration Error", $"Failed to unregister from Claude Code: {ex.Message}", "OK");
- }
- }
-
- private string PerformClientConfiguration(McpClient client)
- {
- // This mirrors the logic from MCPForUnityEditorWindow.ConfigureMcpClient
- string configPath = McpConfigurationHelper.GetClientConfigPath(client);
- string pythonDir = McpPathResolver.FindPackagePythonDirectory();
-
- if (string.IsNullOrEmpty(pythonDir))
- {
- return "Manual configuration required - Python server directory not found.";
- }
-
- McpConfigurationHelper.EnsureConfigDirectoryExists(configPath);
- return McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client);
- }
-
- private void ShowManualSetupInWizard(McpClient client)
- {
- string configPath = McpConfigurationHelper.GetClientConfigPath(client);
- string pythonDir = McpPathResolver.FindPackagePythonDirectory();
- string uvPath = ServerInstaller.FindUvPath();
-
- if (string.IsNullOrEmpty(uvPath))
- {
- EditorUtility.DisplayDialog("Manual Setup", "UV package manager not found. Please install UV first.", "OK");
- return;
- }
-
- // Build manual configuration using the sophisticated helper logic
- string result = McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client);
- string manualConfig;
-
- if (result == "Configured successfully")
- {
- // Read back the configuration that was written
- try
- {
- manualConfig = System.IO.File.ReadAllText(configPath);
- }
- catch
- {
- manualConfig = "Configuration written successfully, but could not read back for display.";
- }
- }
- else
- {
- manualConfig = $"Configuration failed: {result}";
- }
-
- EditorUtility.DisplayDialog(
- $"Manual Setup - {client.name}",
- $"Configuration file location:\n{configPath}\n\n" +
- $"Configuration result:\n{manualConfig}",
- "OK"
- );
- }
-
- private void CheckClientConfiguration(McpClient client)
- {
- // Basic status check - could be enhanced to mirror MCPForUnityEditorWindow logic
- try
- {
- string configPath = McpConfigurationHelper.GetClientConfigPath(client);
- if (System.IO.File.Exists(configPath))
- {
- client.configStatus = "Configured";
- client.status = McpStatus.Configured;
- }
- else
- {
- client.configStatus = "Not Configured";
- client.status = McpStatus.NotConfigured;
- }
- }
- catch
- {
- client.configStatus = "Error";
- client.status = McpStatus.Error;
- }
- }
-
private void OpenInstallationUrls()
{
var (pythonUrl, uvUrl) = DependencyManager.GetInstallationUrls();
diff --git a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
index fdbfcbb5..b8fb3c3a 100644
--- a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
+++ b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
@@ -1,1669 +1,867 @@
using System;
-using System.Collections.Generic;
using System.Diagnostics;
-using System.Security.Cryptography;
-using System.Text;
-using System.Net.Sockets;
-using System.Net;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
using UnityEditor;
+using UnityEditor.UIElements; // For Unity 2021 compatibility
using UnityEngine;
+using UnityEngine.UIElements;
using MCPForUnity.Editor.Data;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Models;
+using MCPForUnity.Editor.Services;
namespace MCPForUnity.Editor.Windows
{
public class MCPForUnityEditorWindow : EditorWindow
{
- private bool isUnityBridgeRunning = false;
- private Vector2 scrollPosition;
- private string pythonServerInstallationStatus = "Not Installed";
- private Color pythonServerInstallationStatusColor = Color.red;
- private const int mcpPort = 6500; // MCP port (still hardcoded for MCP server)
+ // Protocol enum for future HTTP support
+ private enum ConnectionProtocol
+ {
+ Stdio,
+ // HTTPStreaming // Future
+ }
+
+ // Settings UI Elements
+ private Label versionLabel;
+ private Toggle debugLogsToggle;
+ private EnumField validationLevelField;
+ private Label validationDescription;
+ private Foldout advancedSettingsFoldout;
+ private TextField mcpServerPathOverride;
+ private TextField uvPathOverride;
+ private Button browsePythonButton;
+ private Button clearPythonButton;
+ private Button browseUvButton;
+ private Button clearUvButton;
+ private VisualElement mcpServerPathStatus;
+ private VisualElement uvPathStatus;
+
+ // Connection UI Elements
+ private EnumField protocolDropdown;
+ private TextField unityPortField;
+ private TextField serverPortField;
+ private VisualElement statusIndicator;
+ private Label connectionStatusLabel;
+ private Button connectionToggleButton;
+ private VisualElement healthIndicator;
+ private Label healthStatusLabel;
+ private Button testConnectionButton;
+ private VisualElement serverStatusBanner;
+ private Label serverStatusMessage;
+ private Button downloadServerButton;
+ private Button rebuildServerButton;
+
+ // Client UI Elements
+ private DropdownField clientDropdown;
+ private Button configureAllButton;
+ private VisualElement clientStatusIndicator;
+ private Label clientStatusLabel;
+ private Button configureButton;
+ private VisualElement claudeCliPathRow;
+ private TextField claudeCliPath;
+ private Button browseClaudeButton;
+ private Foldout manualConfigFoldout;
+ private TextField configPathField;
+ private Button copyPathButton;
+ private Button openFileButton;
+ private TextField configJsonField;
+ private Button copyJsonButton;
+ private Label installationStepsLabel;
+
+ // Data
private readonly McpClients mcpClients = new();
- private bool autoRegisterEnabled;
- private bool lastClientRegisteredOk;
- private bool lastBridgeVerifiedOk;
- private string pythonDirOverride = null;
- private bool debugLogsEnabled;
-
- // Script validation settings
- private int validationLevelIndex = 1; // Default to Standard
- private readonly string[] validationLevelOptions = new string[]
- {
- "Basic - Only syntax checks",
- "Standard - Syntax + Unity practices",
- "Comprehensive - All checks + semantic analysis",
- "Strict - Full semantic validation (requires Roslyn)"
- };
-
- // UI state
private int selectedClientIndex = 0;
+ private ValidationLevel currentValidationLevel = ValidationLevel.Standard;
- public static void ShowWindow()
+ // Validation levels matching the existing enum
+ private enum ValidationLevel
{
- GetWindow("MCP For Unity");
+ Basic,
+ Standard,
+ Comprehensive,
+ Strict
}
- private void OnEnable()
+ public static void ShowWindow()
+ {
+ var window = GetWindow("MCP For Unity");
+ window.minSize = new Vector2(500, 600);
+ }
+ public void CreateGUI()
{
- UpdatePythonServerInstallationStatus();
+ // Determine base path (Package Manager vs Asset Store install)
+ string basePath = AssetPathUtility.GetMcpPackageRootPath();
- // Refresh bridge status
- isUnityBridgeRunning = MCPForUnityBridge.IsRunning;
- autoRegisterEnabled = EditorPrefs.GetBool("MCPForUnity.AutoRegisterEnabled", true);
- debugLogsEnabled = EditorPrefs.GetBool("MCPForUnity.DebugLogs", false);
- if (debugLogsEnabled)
- {
- LogDebugPrefsState();
- }
- foreach (McpClient mcpClient in mcpClients.clients)
+ // Load UXML
+ var visualTree = AssetDatabase.LoadAssetAtPath(
+ $"{basePath}/Editor/Windows/MCPForUnityEditorWindow.uxml"
+ );
+
+ if (visualTree == null)
{
- CheckMcpConfiguration(mcpClient);
+ McpLog.Error($"Failed to load UXML at: {basePath}/Editor/Windows/MCPForUnityEditorWindow.uxml");
+ return;
}
- // Load validation level setting
- LoadValidationLevelSetting();
+ visualTree.CloneTree(rootVisualElement);
- // First-run auto-setup only if Claude CLI is available
- if (autoRegisterEnabled && !string.IsNullOrEmpty(ExecPath.ResolveClaude()))
- {
- AutoFirstRunSetup();
- }
- }
+ // Load USS
+ var styleSheet = AssetDatabase.LoadAssetAtPath(
+ $"{basePath}/Editor/Windows/MCPForUnityEditorWindow.uss"
+ );
- private void OnFocus()
- {
- // Refresh bridge running state on focus in case initialization completed after domain reload
- isUnityBridgeRunning = MCPForUnityBridge.IsRunning;
- if (mcpClients.clients.Count > 0 && selectedClientIndex < mcpClients.clients.Count)
+ if (styleSheet != null)
{
- McpClient selectedClient = mcpClients.clients[selectedClientIndex];
- CheckMcpConfiguration(selectedClient);
+ rootVisualElement.styleSheets.Add(styleSheet);
}
- Repaint();
+
+ // Cache UI elements
+ CacheUIElements();
+
+ // Initialize UI
+ InitializeUI();
+
+ // Register callbacks
+ RegisterCallbacks();
+
+ // Initial update
+ UpdateConnectionStatus();
+ UpdateServerStatusBanner();
+ UpdateClientStatus();
+ UpdatePathOverrides();
+ // Technically not required to connect, but if we don't do this, the UI will be blank
+ UpdateManualConfiguration();
+ UpdateClaudeCliPathVisibility();
}
- private Color GetStatusColor(McpStatus status)
+ private void OnEnable()
{
- // Return appropriate color based on the status enum
- return status switch
- {
- McpStatus.Configured => Color.green,
- McpStatus.Running => Color.green,
- McpStatus.Connected => Color.green,
- McpStatus.IncorrectPath => Color.yellow,
- McpStatus.CommunicationError => Color.yellow,
- McpStatus.NoResponse => Color.yellow,
- _ => Color.red, // Default to red for error states or not configured
- };
+ EditorApplication.update += OnEditorUpdate;
}
- private void UpdatePythonServerInstallationStatus()
+ private void OnDisable()
{
- try
- {
- string installedPath = ServerInstaller.GetServerPath();
- bool installedOk = !string.IsNullOrEmpty(installedPath) && File.Exists(Path.Combine(installedPath, "server.py"));
- if (installedOk)
- {
- pythonServerInstallationStatus = "Installed";
- pythonServerInstallationStatusColor = Color.green;
- return;
- }
-
- // Fall back to embedded/dev source via our existing resolution logic
- string embeddedPath = FindPackagePythonDirectory();
- bool embeddedOk = !string.IsNullOrEmpty(embeddedPath) && File.Exists(Path.Combine(embeddedPath, "server.py"));
- if (embeddedOk)
- {
- pythonServerInstallationStatus = "Installed (Embedded)";
- pythonServerInstallationStatusColor = Color.green;
- }
- else
- {
- pythonServerInstallationStatus = "Not Installed";
- pythonServerInstallationStatusColor = Color.red;
- }
- }
- catch
- {
- pythonServerInstallationStatus = "Not Installed";
- pythonServerInstallationStatusColor = Color.red;
- }
+ EditorApplication.update -= OnEditorUpdate;
}
-
- private void DrawStatusDot(Rect statusRect, Color statusColor, float size = 12)
+ private void OnFocus()
{
- float offsetX = (statusRect.width - size) / 2;
- float offsetY = (statusRect.height - size) / 2;
- Rect dotRect = new(statusRect.x + offsetX, statusRect.y + offsetY, size, size);
- Vector3 center = new(
- dotRect.x + (dotRect.width / 2),
- dotRect.y + (dotRect.height / 2),
- 0
- );
- float radius = size / 2;
-
- // Draw the main dot
- Handles.color = statusColor;
- Handles.DrawSolidDisc(center, Vector3.forward, radius);
+ // Only refresh data if UI is built
+ if (rootVisualElement == null || rootVisualElement.childCount == 0)
+ return;
- // Draw the border
- Color borderColor = new(
- statusColor.r * 0.7f,
- statusColor.g * 0.7f,
- statusColor.b * 0.7f
- );
- Handles.color = borderColor;
- Handles.DrawWireDisc(center, Vector3.forward, radius);
+ RefreshAllData();
}
- private void OnGUI()
+ private void OnEditorUpdate()
{
- scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
-
- // Header
- DrawHeader();
-
- // Compute equal column widths for uniform layout
- float horizontalSpacing = 2f;
- float outerPadding = 20f; // approximate padding
- // Make columns a bit less wide for a tighter layout
- float computed = (position.width - outerPadding - horizontalSpacing) / 2f;
- float colWidth = Mathf.Clamp(computed, 220f, 340f);
- // Use fixed heights per row so paired panels match exactly
- float topPanelHeight = 190f;
- float bottomPanelHeight = 230f;
-
- // Top row: Server Status (left) and Unity Bridge (right)
- EditorGUILayout.BeginHorizontal();
- {
- EditorGUILayout.BeginVertical(GUILayout.Width(colWidth), GUILayout.Height(topPanelHeight));
- DrawServerStatusSection();
- EditorGUILayout.EndVertical();
+ // Only update UI if it's built
+ if (rootVisualElement == null || rootVisualElement.childCount == 0)
+ return;
- EditorGUILayout.Space(horizontalSpacing);
+ UpdateConnectionStatus();
+ }
+
+ private void RefreshAllData()
+ {
+ // Update connection status
+ UpdateConnectionStatus();
- EditorGUILayout.BeginVertical(GUILayout.Width(colWidth), GUILayout.Height(topPanelHeight));
- DrawBridgeSection();
- EditorGUILayout.EndVertical();
+ // Auto-verify bridge health if connected
+ if (MCPServiceLocator.Bridge.IsRunning)
+ {
+ VerifyBridgeConnection();
}
- EditorGUILayout.EndHorizontal();
- EditorGUILayout.Space(10);
+ // Update path overrides
+ UpdatePathOverrides();
- // Second row: MCP Client Configuration (left) and Script Validation (right)
- EditorGUILayout.BeginHorizontal();
+ // Refresh selected client (may have been configured externally)
+ if (selectedClientIndex >= 0 && selectedClientIndex < mcpClients.clients.Count)
{
- EditorGUILayout.BeginVertical(GUILayout.Width(colWidth), GUILayout.Height(bottomPanelHeight));
- DrawUnifiedClientConfiguration();
- EditorGUILayout.EndVertical();
-
- EditorGUILayout.Space(horizontalSpacing);
-
- EditorGUILayout.BeginVertical(GUILayout.Width(colWidth), GUILayout.Height(bottomPanelHeight));
- DrawValidationSection();
- EditorGUILayout.EndVertical();
+ var client = mcpClients.clients[selectedClientIndex];
+ MCPServiceLocator.Client.CheckClientStatus(client);
+ UpdateClientStatus();
+ UpdateManualConfiguration();
+ UpdateClaudeCliPathVisibility();
}
- EditorGUILayout.EndHorizontal();
+ }
- // Minimal bottom padding
- EditorGUILayout.Space(2);
+ private void CacheUIElements()
+ {
+ // Settings
+ versionLabel = rootVisualElement.Q