diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml
index 3ce232b7..0346f15b 100644
--- a/.github/workflows/bump-version.yml
+++ b/.github/workflows/bump-version.yml
@@ -108,3 +108,30 @@ jobs:
git tag -a "$TAG" -m "Version ${NEW_VERSION}"
git push origin "$TAG"
+
+ - name: Package server for release
+ env:
+ NEW_VERSION: ${{ steps.compute.outputs.new_version }}
+ shell: bash
+ run: |
+ set -euo pipefail
+ cd MCPForUnity/UnityMcpServer~
+ zip -r ../../mcp-for-unity-server-v${NEW_VERSION}.zip .
+ cd ../..
+ ls -lh mcp-for-unity-server-v${NEW_VERSION}.zip
+ echo "Server package created: mcp-for-unity-server-v${NEW_VERSION}.zip"
+
+ - name: Create GitHub release with server artifact
+ env:
+ NEW_VERSION: ${{ steps.compute.outputs.new_version }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ shell: bash
+ run: |
+ set -euo pipefail
+ TAG="v${NEW_VERSION}"
+
+ # Create release
+ gh release create "$TAG" \
+ --title "v${NEW_VERSION}" \
+ --notes "Release v${NEW_VERSION}" \
+ "mcp-for-unity-server-v${NEW_VERSION}.zip#MCP Server v${NEW_VERSION}"
diff --git a/MCPForUnity/Editor/Helpers/AssetPathUtility.cs b/MCPForUnity/Editor/Helpers/AssetPathUtility.cs
index f03b66c7..dac1facf 100644
--- a/MCPForUnity/Editor/Helpers/AssetPathUtility.cs
+++ b/MCPForUnity/Editor/Helpers/AssetPathUtility.cs
@@ -1,4 +1,9 @@
using System;
+using System.IO;
+using Newtonsoft.Json.Linq;
+using UnityEditor;
+using UnityEngine;
+using PackageInfo = UnityEditor.PackageManager.PackageInfo;
namespace MCPForUnity.Editor.Helpers
{
@@ -25,5 +30,133 @@ public static string SanitizeAssetPath(string path)
return path;
}
+
+ ///
+ /// Gets the MCP for Unity package root path.
+ /// Works for registry Package Manager, local Package Manager, and Asset Store installations.
+ ///
+ /// The package root path (virtual for PM, absolute for Asset Store), or null if not found
+ public static string GetMcpPackageRootPath()
+ {
+ try
+ {
+ // Try Package Manager first (registry and local installs)
+ var packageInfo = PackageInfo.FindForAssembly(typeof(AssetPathUtility).Assembly);
+ if (packageInfo != null && !string.IsNullOrEmpty(packageInfo.assetPath))
+ {
+ return packageInfo.assetPath;
+ }
+
+ // Fallback to AssetDatabase for Asset Store installs (Assets/MCPForUnity)
+ string[] guids = AssetDatabase.FindAssets($"t:Script {nameof(AssetPathUtility)}");
+
+ if (guids.Length == 0)
+ {
+ McpLog.Warn("Could not find AssetPathUtility script in AssetDatabase");
+ return null;
+ }
+
+ string scriptPath = AssetDatabase.GUIDToAssetPath(guids[0]);
+
+ // Script is at: {packageRoot}/Editor/Helpers/AssetPathUtility.cs
+ // Extract {packageRoot}
+ int editorIndex = scriptPath.IndexOf("/Editor/", StringComparison.Ordinal);
+
+ if (editorIndex >= 0)
+ {
+ return scriptPath.Substring(0, editorIndex);
+ }
+
+ McpLog.Warn($"Could not determine package root from script path: {scriptPath}");
+ return null;
+ }
+ catch (Exception ex)
+ {
+ McpLog.Error($"Failed to get package root path: {ex.Message}");
+ return null;
+ }
+ }
+
+ ///
+ /// Reads and parses the package.json file for MCP for Unity.
+ /// Handles both Package Manager (registry/local) and Asset Store installations.
+ ///
+ /// JObject containing package.json data, or null if not found or parse failed
+ public static JObject GetPackageJson()
+ {
+ try
+ {
+ string packageRoot = GetMcpPackageRootPath();
+ if (string.IsNullOrEmpty(packageRoot))
+ {
+ return null;
+ }
+
+ string packageJsonPath = Path.Combine(packageRoot, "package.json");
+
+ // Convert virtual asset path to file system path
+ if (packageRoot.StartsWith("Packages/", StringComparison.OrdinalIgnoreCase))
+ {
+ // Package Manager install - must use PackageInfo.resolvedPath
+ // Virtual paths like "Packages/..." don't work with File.Exists()
+ // Registry packages live in Library/PackageCache/package@version/
+ var packageInfo = PackageInfo.FindForAssembly(typeof(AssetPathUtility).Assembly);
+ if (packageInfo != null && !string.IsNullOrEmpty(packageInfo.resolvedPath))
+ {
+ packageJsonPath = Path.Combine(packageInfo.resolvedPath, "package.json");
+ }
+ else
+ {
+ McpLog.Warn("Could not resolve Package Manager path for package.json");
+ return null;
+ }
+ }
+ else if (packageRoot.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
+ {
+ // Asset Store install - convert to absolute file system path
+ // Application.dataPath is the absolute path to the Assets folder
+ string relativePath = packageRoot.Substring("Assets/".Length);
+ packageJsonPath = Path.Combine(Application.dataPath, relativePath, "package.json");
+ }
+
+ if (!File.Exists(packageJsonPath))
+ {
+ McpLog.Warn($"package.json not found at: {packageJsonPath}");
+ return null;
+ }
+
+ string json = File.ReadAllText(packageJsonPath);
+ return JObject.Parse(json);
+ }
+ catch (Exception ex)
+ {
+ McpLog.Warn($"Failed to read or parse package.json: {ex.Message}");
+ return null;
+ }
+ }
+
+ ///
+ /// Gets the version string from the package.json file.
+ ///
+ /// Version string, or "unknown" if not found
+ public static string GetPackageVersion()
+ {
+ try
+ {
+ var packageJson = GetPackageJson();
+ if (packageJson == null)
+ {
+ return "unknown";
+ }
+
+ string version = packageJson["version"]?.ToString();
+ return string.IsNullOrEmpty(version) ? "unknown" : version;
+ }
+ catch (Exception ex)
+ {
+ McpLog.Warn($"Failed to get package version: {ex.Message}");
+ return "unknown";
+ }
+ }
}
}
diff --git a/MCPForUnity/Editor/Helpers/McpPathResolver.cs b/MCPForUnity/Editor/Helpers/McpPathResolver.cs
index 8e683965..be1089f7 100644
--- a/MCPForUnity/Editor/Helpers/McpPathResolver.cs
+++ b/MCPForUnity/Editor/Helpers/McpPathResolver.cs
@@ -7,7 +7,7 @@
namespace MCPForUnity.Editor.Helpers
{
///
- /// Shared helper for resolving Python server directory paths with support for
+ /// Shared helper for resolving MCP server directory paths with support for
/// development mode, embedded servers, and installed packages
///
public static class McpPathResolver
@@ -15,7 +15,7 @@ public static class McpPathResolver
private const string USE_EMBEDDED_SERVER_KEY = "MCPForUnity.UseEmbeddedServer";
///
- /// Resolves the Python server directory path with comprehensive logic
+ /// Resolves the MCP server directory path with comprehensive logic
/// including development mode support and fallback mechanisms
///
public static string FindPackagePythonDirectory(bool debugLogsEnabled = false)
diff --git a/MCPForUnity/Editor/Helpers/PackageDetector.cs b/MCPForUnity/Editor/Helpers/PackageDetector.cs
index bb8861fe..59e22348 100644
--- a/MCPForUnity/Editor/Helpers/PackageDetector.cs
+++ b/MCPForUnity/Editor/Helpers/PackageDetector.cs
@@ -49,8 +49,7 @@ static PackageDetector()
if (!string.IsNullOrEmpty(error))
{
- Debug.LogWarning($"MCP for Unity: Auto-detect on load failed: {capturedEx}");
- // Alternatively: Debug.LogException(capturedEx);
+ McpLog.Info($"Server check: {error}. Download via Window > MCP For Unity if needed.", always: false);
}
};
}
diff --git a/MCPForUnity/Editor/Helpers/PackageInstaller.cs b/MCPForUnity/Editor/Helpers/PackageInstaller.cs
index 031a6aed..1d46f321 100644
--- a/MCPForUnity/Editor/Helpers/PackageInstaller.cs
+++ b/MCPForUnity/Editor/Helpers/PackageInstaller.cs
@@ -4,7 +4,7 @@
namespace MCPForUnity.Editor.Helpers
{
///
- /// Handles automatic installation of the Python server when the package is first installed.
+ /// Handles automatic installation of the MCP server when the package is first installed.
///
[InitializeOnLoad]
public static class PackageInstaller
@@ -25,18 +25,21 @@ private static void InstallServerOnFirstLoad()
{
try
{
- Debug.Log("MCP-FOR-UNITY: Installing Python server...");
ServerInstaller.EnsureServerInstalled();
- // Mark as installed
+ // Mark as installed/checked
EditorPrefs.SetBool(InstallationFlagKey, true);
- Debug.Log("MCP-FOR-UNITY: Python server installation completed successfully.");
+ // Only log success if server was actually embedded and copied
+ if (ServerInstaller.HasEmbeddedServer())
+ {
+ McpLog.Info("MCP server installation completed successfully.");
+ }
}
- catch (System.Exception ex)
+ catch (System.Exception)
{
- Debug.LogError($"MCP-FOR-UNITY: Failed to install Python server: {ex.Message}");
- Debug.LogWarning("MCP-FOR-UNITY: You may need to manually install the Python server. Check the MCP For Unity Window for instructions.");
+ 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/ServerInstaller.cs b/MCPForUnity/Editor/Helpers/ServerInstaller.cs
index f41e03c3..5ab823eb 100644
--- a/MCPForUnity/Editor/Helpers/ServerInstaller.cs
+++ b/MCPForUnity/Editor/Helpers/ServerInstaller.cs
@@ -1,9 +1,11 @@
using System;
-using System.IO;
-using System.Runtime.InteropServices;
-using System.Text;
using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.IO.Compression;
using System.Linq;
+using System.Net;
+using System.Runtime.InteropServices;
using UnityEditor;
using UnityEngine;
@@ -34,8 +36,19 @@ public static void EnsureServerInstalled()
// Resolve embedded source and versions
if (!TryGetEmbeddedServerSource(out string embeddedSrc))
{
- throw new Exception("Could not find embedded UnityMcpServer/src in the package.");
+ // Asset Store install - no embedded server
+ // Check if server was already downloaded
+ if (File.Exists(Path.Combine(destSrc, "server.py")))
+ {
+ McpLog.Info("Using previously downloaded MCP server.", always: false);
+ }
+ else
+ {
+ McpLog.Info("MCP server not found. Download via Window > MCP For Unity > Open MCP Window.", always: false);
+ }
+ return; // Graceful exit - no exception
}
+
string embeddedVer = ReadVersionFile(Path.Combine(embeddedSrc, VersionFileName)) ?? "unknown";
string installedVer = ReadVersionFile(Path.Combine(destSrc, VersionFileName));
@@ -151,7 +164,7 @@ private static string GetSaveLocation()
TryCreateMacSymlinkForAppSupport();
return Path.Combine(localAppSupport, RootFolder);
}
- throw new Exception("Unsupported operating system.");
+ throw new Exception("Unsupported operating system");
}
///
@@ -177,7 +190,7 @@ private static void TryCreateMacSymlinkForAppSupport()
if (!Directory.Exists(canonical)) return;
// Use 'ln -s' to create a directory symlink (macOS)
- var psi = new System.Diagnostics.ProcessStartInfo
+ var psi = new ProcessStartInfo
{
FileName = "/bin/ln",
Arguments = $"-s \"{canonical}\" \"{symlink}\"",
@@ -186,7 +199,7 @@ private static void TryCreateMacSymlinkForAppSupport()
RedirectStandardError = true,
CreateNoWindow = true
};
- using var p = System.Diagnostics.Process.Start(psi);
+ using var p = Process.Start(psi);
p?.WaitForExit(2000);
}
catch { /* best-effort */ }
@@ -303,7 +316,7 @@ private static bool PathsEqualSafe(string a, string b)
private static IEnumerable GetLegacyRootsForDetection()
{
- var roots = new System.Collections.Generic.List();
+ var roots = new List();
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
// macOS/Linux legacy
roots.Add(Path.Combine(home, ".config", "UnityMCP", "UnityMcpServer"));
@@ -331,7 +344,7 @@ private static void TryKillUvForPath(string serverSrcPath)
if (string.IsNullOrEmpty(serverSrcPath)) return;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return;
- var psi = new System.Diagnostics.ProcessStartInfo
+ var psi = new ProcessStartInfo
{
FileName = "/usr/bin/pgrep",
Arguments = $"-f \"uv .*--directory {serverSrcPath}\"",
@@ -340,7 +353,7 @@ private static void TryKillUvForPath(string serverSrcPath)
RedirectStandardError = true,
CreateNoWindow = true
};
- using var p = System.Diagnostics.Process.Start(psi);
+ using var p = Process.Start(psi);
if (p == null) return;
string outp = p.StandardOutput.ReadToEnd();
p.WaitForExit(1500);
@@ -350,7 +363,7 @@ private static void TryKillUvForPath(string serverSrcPath)
{
if (int.TryParse(line.Trim(), out int pid))
{
- try { System.Diagnostics.Process.GetProcessById(pid).Kill(); } catch { }
+ try { Process.GetProcessById(pid).Kill(); } catch { }
}
}
}
@@ -430,7 +443,7 @@ public static bool RebuildMcpServer()
// Find embedded source
if (!TryGetEmbeddedServerSource(out string embeddedSrc))
{
- Debug.LogError("RebuildMcpServer: Could not find embedded server source.");
+ McpLog.Error("RebuildMcpServer: Could not find embedded server source.");
return false;
}
@@ -447,11 +460,11 @@ public static bool RebuildMcpServer()
try
{
Directory.Delete(destRoot, recursive: true);
- Debug.Log($"MCP-FOR-UNITY: Deleted existing server at {destRoot}");
+ McpLog.Info($"Deleted existing server at {destRoot}");
}
catch (Exception ex)
{
- Debug.LogError($"Failed to delete existing server: {ex.Message}");
+ McpLog.Error($"Failed to delete existing server: {ex.Message}");
return false;
}
}
@@ -469,15 +482,15 @@ public static bool RebuildMcpServer()
}
catch (Exception ex)
{
- Debug.LogWarning($"Failed to write version file: {ex.Message}");
+ McpLog.Warn($"Failed to write version file: {ex.Message}");
}
- Debug.Log($"MCP-FOR-UNITY: Server rebuilt successfully at {destRoot} (version {embeddedVer})");
+ McpLog.Info($"Server rebuilt successfully at {destRoot} (version {embeddedVer})");
return true;
}
catch (Exception ex)
{
- Debug.LogError($"RebuildMcpServer failed: {ex.Message}");
+ McpLog.Error($"RebuildMcpServer failed: {ex.Message}");
return false;
}
}
@@ -508,7 +521,7 @@ internal static string FindUvPath()
// Fast path: resolve from PATH first
try
{
- var wherePsi = new System.Diagnostics.ProcessStartInfo
+ var wherePsi = new ProcessStartInfo
{
FileName = "where",
Arguments = "uv.exe",
@@ -517,7 +530,7 @@ internal static string FindUvPath()
RedirectStandardError = true,
CreateNoWindow = true
};
- using var wp = System.Diagnostics.Process.Start(wherePsi);
+ using var wp = Process.Start(wherePsi);
string output = wp.StandardOutput.ReadToEnd().Trim();
wp.WaitForExit(1500);
if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output))
@@ -613,7 +626,7 @@ internal static string FindUvPath()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
- var whichPsi = new System.Diagnostics.ProcessStartInfo
+ var whichPsi = new ProcessStartInfo
{
FileName = "/usr/bin/which",
Arguments = "uv",
@@ -628,7 +641,7 @@ internal static string FindUvPath()
string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
string prepend = string.Join(":", new[]
{
- System.IO.Path.Combine(homeDir, ".local", "bin"),
+ Path.Combine(homeDir, ".local", "bin"),
"/opt/homebrew/bin",
"/usr/local/bin",
"/usr/bin",
@@ -638,7 +651,7 @@ internal static string FindUvPath()
whichPsi.EnvironmentVariables["PATH"] = string.IsNullOrEmpty(currentPath) ? prepend : (prepend + ":" + currentPath);
}
catch { }
- using var wp = System.Diagnostics.Process.Start(whichPsi);
+ using var wp = Process.Start(whichPsi);
string output = wp.StandardOutput.ReadToEnd().Trim();
wp.WaitForExit(3000);
if (wp.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output))
@@ -676,7 +689,7 @@ private static bool ValidateUvBinary(string uvPath)
{
try
{
- var psi = new System.Diagnostics.ProcessStartInfo
+ var psi = new ProcessStartInfo
{
FileName = uvPath,
Arguments = "--version",
@@ -685,7 +698,7 @@ private static bool ValidateUvBinary(string uvPath)
RedirectStandardError = true,
CreateNoWindow = true
};
- using var p = System.Diagnostics.Process.Start(psi);
+ using var p = Process.Start(psi);
if (!p.WaitForExit(5000)) { try { p.Kill(); } catch { } return false; }
if (p.ExitCode == 0)
{
@@ -696,5 +709,133 @@ private static bool ValidateUvBinary(string uvPath)
catch { }
return false;
}
+
+ ///
+ /// Download and install server from GitHub release (Asset Store workflow)
+ ///
+ public static bool DownloadAndInstallServer()
+ {
+ string packageVersion = AssetPathUtility.GetPackageVersion();
+ if (packageVersion == "unknown")
+ {
+ McpLog.Error("Cannot determine package version for download.");
+ return false;
+ }
+
+ string downloadUrl = $"https://github.com/CoplayDev/unity-mcp/releases/download/v{packageVersion}/mcp-for-unity-server-v{packageVersion}.zip";
+ string tempZip = Path.Combine(Path.GetTempPath(), $"mcp-server-v{packageVersion}.zip");
+ string destRoot = Path.Combine(GetSaveLocation(), ServerFolder);
+
+ try
+ {
+ EditorUtility.DisplayProgressBar("MCP for Unity", "Downloading server...", 0.3f);
+
+ // Download
+ using (var client = new WebClient())
+ {
+ client.DownloadFile(downloadUrl, tempZip);
+ }
+
+ EditorUtility.DisplayProgressBar("MCP for Unity", "Extracting server...", 0.7f);
+
+ // Kill any running UV processes
+ string destSrc = Path.Combine(destRoot, "src");
+ TryKillUvForPath(destSrc);
+
+ // Delete old installation
+ if (Directory.Exists(destRoot))
+ {
+ try
+ {
+ Directory.Delete(destRoot, recursive: true);
+ }
+ catch (Exception ex)
+ {
+ McpLog.Warn($"Could not fully delete old server: {ex.Message}");
+ }
+ }
+
+ // Extract to temp location first
+ string tempExtractDir = Path.Combine(Path.GetTempPath(), $"mcp-server-extract-{Guid.NewGuid()}");
+ Directory.CreateDirectory(tempExtractDir);
+
+ try
+ {
+ ZipFile.ExtractToDirectory(tempZip, tempExtractDir);
+
+ // The ZIP contains UnityMcpServer~ folder, find it and move its contents
+ string extractedServerFolder = Path.Combine(tempExtractDir, "UnityMcpServer~");
+ Directory.CreateDirectory(destRoot);
+ CopyDirectoryRecursive(extractedServerFolder, destRoot);
+ }
+ finally
+ {
+ // Cleanup temp extraction directory
+ try
+ {
+ if (Directory.Exists(tempExtractDir))
+ {
+ Directory.Delete(tempExtractDir, recursive: true);
+ }
+ }
+ catch (Exception ex)
+ {
+ McpLog.Warn($"Could not fully delete temp extraction directory: {ex.Message}");
+ }
+ }
+
+ EditorUtility.ClearProgressBar();
+ McpLog.Info($"Server v{packageVersion} downloaded and installed successfully!");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ EditorUtility.ClearProgressBar();
+ McpLog.Error($"Failed to download server: {ex.Message}");
+ EditorUtility.DisplayDialog(
+ "Download Failed",
+ $"Could not download server from GitHub.\n\n{ex.Message}\n\nPlease check your internet connection or try again later.",
+ "OK"
+ );
+ return false;
+ }
+ finally
+ {
+ try {
+ if (File.Exists(tempZip)) File.Delete(tempZip);
+ } catch (Exception ex) {
+ McpLog.Warn($"Could not delete temp zip file: {ex.Message}");
+ }
+ }
+ }
+
+ ///
+ /// Check if the package has an embedded server (Git install vs Asset Store)
+ ///
+ public static bool HasEmbeddedServer()
+ {
+ return TryGetEmbeddedServerSource(out _);
+ }
+
+ ///
+ /// Get the installed server version from the local installation
+ ///
+ public static string GetInstalledServerVersion()
+ {
+ try
+ {
+ string destRoot = Path.Combine(GetSaveLocation(), ServerFolder);
+ string versionPath = Path.Combine(destRoot, "src", VersionFileName);
+ if (File.Exists(versionPath))
+ {
+ return File.ReadAllText(versionPath)?.Trim() ?? string.Empty;
+ }
+ }
+ catch (Exception ex)
+ {
+ McpLog.Warn($"Could not read version file: {ex.Message}");
+ }
+ return string.Empty;
+ }
}
}
diff --git a/MCPForUnity/Editor/Helpers/TelemetryHelper.cs b/MCPForUnity/Editor/Helpers/TelemetryHelper.cs
index 6440a675..0f436231 100644
--- a/MCPForUnity/Editor/Helpers/TelemetryHelper.cs
+++ b/MCPForUnity/Editor/Helpers/TelemetryHelper.cs
@@ -81,8 +81,8 @@ public static void EnableTelemetry()
}
///
- /// Send telemetry data to Python server for processing
- /// This is a lightweight bridge - the actual telemetry logic is in Python
+ /// Send telemetry data to MCP server for processing
+ /// This is a lightweight bridge - the actual telemetry logic is in the MCP server
///
public static void RecordEvent(string eventType, Dictionary data = null)
{
@@ -106,16 +106,16 @@ public static void RecordEvent(string eventType, Dictionary data
telemetryData["data"] = data;
}
- // Send to Python server via existing bridge communication
- // The Python server will handle actual telemetry transmission
- SendTelemetryToPythonServer(telemetryData);
+ // Send to MCP server via existing bridge communication
+ // The MCP server will handle actual telemetry transmission
+ SendTelemetryToMcpServer(telemetryData);
}
catch (Exception e)
{
// Never let telemetry errors interfere with functionality
if (IsDebugEnabled())
{
- Debug.LogWarning($"Telemetry error (non-blocking): {e.Message}");
+ McpLog.Warn($"Telemetry error (non-blocking): {e.Message}");
}
}
}
@@ -183,7 +183,7 @@ public static void RecordToolExecution(string toolName, bool success, float dura
RecordEvent("tool_execution_unity", data);
}
- private static void SendTelemetryToPythonServer(Dictionary telemetryData)
+ private static void SendTelemetryToMcpServer(Dictionary telemetryData)
{
var sender = Volatile.Read(ref s_sender);
if (sender != null)
@@ -197,7 +197,7 @@ private static void SendTelemetryToPythonServer(Dictionary telem
{
if (IsDebugEnabled())
{
- Debug.LogWarning($"Telemetry sender error (non-blocking): {e.Message}");
+ McpLog.Warn($"Telemetry sender error (non-blocking): {e.Message}");
}
}
}
@@ -205,7 +205,7 @@ private static void SendTelemetryToPythonServer(Dictionary telem
// Fallback: log when debug is enabled
if (IsDebugEnabled())
{
- Debug.Log($"MCP-TELEMETRY: {telemetryData["event_type"]}");
+ McpLog.Info($"Telemetry: {telemetryData["event_type"]}");
}
}
diff --git a/MCPForUnity/Editor/Services.meta b/MCPForUnity/Editor/Services.meta
new file mode 100644
index 00000000..e800deae
--- /dev/null
+++ b/MCPForUnity/Editor/Services.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 2ab6b1cc527214416b21e07b96164f24
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/MCPForUnity/Editor/Services/BridgeControlService.cs b/MCPForUnity/Editor/Services/BridgeControlService.cs
new file mode 100644
index 00000000..a462e68b
--- /dev/null
+++ b/MCPForUnity/Editor/Services/BridgeControlService.cs
@@ -0,0 +1,174 @@
+using System;
+using System.IO;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+
+namespace MCPForUnity.Editor.Services
+{
+ ///
+ /// Implementation of bridge control service
+ ///
+ public class BridgeControlService : IBridgeControlService
+ {
+ public bool IsRunning => MCPForUnityBridge.IsRunning;
+ public int CurrentPort => MCPForUnityBridge.GetCurrentPort();
+ public bool IsAutoConnectMode => MCPForUnityBridge.IsAutoConnectMode();
+
+ public void Start()
+ {
+ // If server is installed, use auto-connect mode
+ // Otherwise use standard mode
+ string serverPath = MCPServiceLocator.Paths.GetMcpServerPath();
+ if (!string.IsNullOrEmpty(serverPath) && File.Exists(Path.Combine(serverPath, "server.py")))
+ {
+ MCPForUnityBridge.StartAutoConnect();
+ }
+ else
+ {
+ MCPForUnityBridge.Start();
+ }
+ }
+
+ public void Stop()
+ {
+ MCPForUnityBridge.Stop();
+ }
+
+ public BridgeVerificationResult Verify(int port)
+ {
+ var result = new BridgeVerificationResult
+ {
+ Success = false,
+ HandshakeValid = false,
+ PingSucceeded = false,
+ Message = "Verification not started"
+ };
+
+ const int ConnectTimeoutMs = 1000;
+ const int FrameTimeoutMs = 30000; // Match bridge frame I/O timeout
+
+ try
+ {
+ using (var client = new TcpClient())
+ {
+ // Attempt connection
+ var connectTask = client.ConnectAsync(IPAddress.Loopback, port);
+ if (!connectTask.Wait(ConnectTimeoutMs))
+ {
+ result.Message = "Connection timeout";
+ return result;
+ }
+
+ using (var stream = client.GetStream())
+ {
+ try { client.NoDelay = true; } catch { }
+
+ // 1) Read handshake line (ASCII, newline-terminated)
+ string handshake = ReadLineAscii(stream, 2000);
+ if (string.IsNullOrEmpty(handshake) || handshake.IndexOf("FRAMING=1", StringComparison.OrdinalIgnoreCase) < 0)
+ {
+ result.Message = "Bridge handshake missing FRAMING=1";
+ return result;
+ }
+
+ result.HandshakeValid = true;
+
+ // 2) Send framed "ping"
+ byte[] payload = Encoding.UTF8.GetBytes("ping");
+ WriteFrame(stream, payload, FrameTimeoutMs);
+
+ // 3) Read framed response and check for pong
+ string response = ReadFrameUtf8(stream, FrameTimeoutMs);
+ if (!string.IsNullOrEmpty(response) && response.IndexOf("pong", StringComparison.OrdinalIgnoreCase) >= 0)
+ {
+ result.PingSucceeded = true;
+ result.Success = true;
+ result.Message = "Bridge verified successfully";
+ }
+ else
+ {
+ result.Message = $"Ping failed; response='{response}'";
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ result.Message = $"Verification error: {ex.Message}";
+ }
+
+ return result;
+ }
+
+ // Minimal framing helpers (8-byte big-endian length prefix), blocking with timeouts
+ private static void WriteFrame(NetworkStream stream, byte[] payload, int timeoutMs)
+ {
+ if (payload == null) throw new ArgumentNullException(nameof(payload));
+ if (payload.LongLength < 1) throw new IOException("Zero-length frames are not allowed");
+
+ byte[] header = new byte[8];
+ ulong len = (ulong)payload.LongLength;
+ header[0] = (byte)(len >> 56);
+ header[1] = (byte)(len >> 48);
+ header[2] = (byte)(len >> 40);
+ header[3] = (byte)(len >> 32);
+ header[4] = (byte)(len >> 24);
+ header[5] = (byte)(len >> 16);
+ header[6] = (byte)(len >> 8);
+ header[7] = (byte)(len);
+
+ stream.WriteTimeout = timeoutMs;
+ stream.Write(header, 0, header.Length);
+ stream.Write(payload, 0, payload.Length);
+ }
+
+ private static string ReadFrameUtf8(NetworkStream stream, int timeoutMs)
+ {
+ byte[] header = ReadExact(stream, 8, timeoutMs);
+ ulong len = ((ulong)header[0] << 56)
+ | ((ulong)header[1] << 48)
+ | ((ulong)header[2] << 40)
+ | ((ulong)header[3] << 32)
+ | ((ulong)header[4] << 24)
+ | ((ulong)header[5] << 16)
+ | ((ulong)header[6] << 8)
+ | header[7];
+ if (len == 0UL) throw new IOException("Zero-length frames are not allowed");
+ if (len > int.MaxValue) throw new IOException("Frame too large");
+ byte[] payload = ReadExact(stream, (int)len, timeoutMs);
+ return Encoding.UTF8.GetString(payload);
+ }
+
+ private static byte[] ReadExact(NetworkStream stream, int count, int timeoutMs)
+ {
+ byte[] buffer = new byte[count];
+ int offset = 0;
+ stream.ReadTimeout = timeoutMs;
+ while (offset < count)
+ {
+ int read = stream.Read(buffer, offset, count - offset);
+ if (read <= 0) throw new IOException("Connection closed before reading expected bytes");
+ offset += read;
+ }
+ return buffer;
+ }
+
+ private static string ReadLineAscii(NetworkStream stream, int timeoutMs, int maxLen = 512)
+ {
+ stream.ReadTimeout = timeoutMs;
+ using (var ms = new MemoryStream())
+ {
+ byte[] one = new byte[1];
+ while (ms.Length < maxLen)
+ {
+ int n = stream.Read(one, 0, 1);
+ if (n <= 0) break;
+ if (one[0] == (byte)'\n') break;
+ ms.WriteByte(one[0]);
+ }
+ return Encoding.ASCII.GetString(ms.ToArray());
+ }
+ }
+ }
+}
diff --git a/MCPForUnity/Editor/Services/BridgeControlService.cs.meta b/MCPForUnity/Editor/Services/BridgeControlService.cs.meta
new file mode 100644
index 00000000..93966a28
--- /dev/null
+++ b/MCPForUnity/Editor/Services/BridgeControlService.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ed4f9f69d84a945248dafc0f0b5a62dd
+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
new file mode 100644
index 00000000..39728e01
--- /dev/null
+++ b/MCPForUnity/Editor/Services/ClientConfigurationService.cs
@@ -0,0 +1,502 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using MCPForUnity.Editor.Data;
+using MCPForUnity.Editor.Helpers;
+using MCPForUnity.Editor.Models;
+using Newtonsoft.Json;
+using UnityEditor;
+using UnityEngine;
+
+namespace MCPForUnity.Editor.Services
+{
+ ///
+ /// Implementation of client configuration service
+ ///
+ public class ClientConfigurationService : IClientConfigurationService
+ {
+ private readonly Data.McpClients mcpClients = new();
+
+ public void ConfigureClient(McpClient client)
+ {
+ try
+ {
+ string configPath = McpConfigurationHelper.GetClientConfigPath(client);
+ McpConfigurationHelper.EnsureConfigDirectoryExists(configPath);
+
+ string pythonDir = MCPServiceLocator.Paths.GetMcpServerPath();
+
+ if (pythonDir == null || !File.Exists(Path.Combine(pythonDir, "server.py")))
+ {
+ throw new InvalidOperationException("Server not found. Please use manual configuration or set server path in Advanced Settings.");
+ }
+
+ string result = client.mcpType == McpTypes.Codex
+ ? McpConfigurationHelper.ConfigureCodexClient(pythonDir, configPath, client)
+ : McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client);
+
+ if (result == "Configured successfully")
+ {
+ client.SetStatus(McpStatus.Configured);
+ Debug.Log($"MCP-FOR-UNITY: {client.name} configured successfully");
+ }
+ else
+ {
+ Debug.LogWarning($"Configuration completed with message: {result}");
+ }
+
+ CheckClientStatus(client);
+ }
+ catch (Exception ex)
+ {
+ Debug.LogError($"Failed to configure {client.name}: {ex.Message}");
+ throw;
+ }
+ }
+
+ public ClientConfigurationSummary ConfigureAllDetectedClients()
+ {
+ var summary = new ClientConfigurationSummary();
+ var pathService = MCPServiceLocator.Paths;
+
+ foreach (var client in mcpClients.clients)
+ {
+ try
+ {
+ // Skip if already configured
+ CheckClientStatus(client, attemptAutoRewrite: false);
+ if (client.status == McpStatus.Configured)
+ {
+ summary.SkippedCount++;
+ summary.Messages.Add($"✓ {client.name}: Already configured");
+ continue;
+ }
+
+ // Check if required tools are available
+ if (client.mcpType == McpTypes.ClaudeCode)
+ {
+ if (!pathService.IsClaudeCliDetected())
+ {
+ summary.SkippedCount++;
+ summary.Messages.Add($"➜ {client.name}: Claude CLI not found");
+ continue;
+ }
+
+ RegisterClaudeCode();
+ summary.SuccessCount++;
+ summary.Messages.Add($"✓ {client.name}: Registered successfully");
+ }
+ else
+ {
+ // Other clients require UV
+ if (!pathService.IsUvDetected())
+ {
+ summary.SkippedCount++;
+ summary.Messages.Add($"➜ {client.name}: UV not found");
+ continue;
+ }
+
+ ConfigureClient(client);
+ summary.SuccessCount++;
+ summary.Messages.Add($"✓ {client.name}: Configured successfully");
+ }
+ }
+ catch (Exception ex)
+ {
+ summary.FailureCount++;
+ summary.Messages.Add($"⚠ {client.name}: {ex.Message}");
+ }
+ }
+
+ return summary;
+ }
+
+ public bool CheckClientStatus(McpClient client, bool attemptAutoRewrite = true)
+ {
+ var previousStatus = client.status;
+
+ try
+ {
+ // Special handling for Claude Code
+ if (client.mcpType == McpTypes.ClaudeCode)
+ {
+ CheckClaudeCodeConfiguration(client);
+ return client.status != previousStatus;
+ }
+
+ string configPath = McpConfigurationHelper.GetClientConfigPath(client);
+
+ if (!File.Exists(configPath))
+ {
+ client.SetStatus(McpStatus.NotConfigured);
+ return client.status != previousStatus;
+ }
+
+ string configJson = File.ReadAllText(configPath);
+ string pythonDir = MCPServiceLocator.Paths.GetMcpServerPath();
+
+ // Check configuration based on client type
+ string[] args = null;
+ bool configExists = false;
+
+ switch (client.mcpType)
+ {
+ case McpTypes.VSCode:
+ dynamic vsConfig = JsonConvert.DeserializeObject(configJson);
+ if (vsConfig?.servers?.unityMCP != null)
+ {
+ args = vsConfig.servers.unityMCP.args.ToObject();
+ configExists = true;
+ }
+ else if (vsConfig?.mcp?.servers?.unityMCP != null)
+ {
+ args = vsConfig.mcp.servers.unityMCP.args.ToObject();
+ configExists = true;
+ }
+ break;
+
+ case McpTypes.Codex:
+ if (CodexConfigHelper.TryParseCodexServer(configJson, out _, out var codexArgs))
+ {
+ args = codexArgs;
+ configExists = true;
+ }
+ break;
+
+ default:
+ McpConfig standardConfig = JsonConvert.DeserializeObject(configJson);
+ if (standardConfig?.mcpServers?.unityMCP != null)
+ {
+ args = standardConfig.mcpServers.unityMCP.args;
+ configExists = true;
+ }
+ break;
+ }
+
+ if (configExists)
+ {
+ string configuredDir = McpConfigFileHelper.ExtractDirectoryArg(args);
+ bool matches = !string.IsNullOrEmpty(configuredDir) &&
+ McpConfigFileHelper.PathsEqual(configuredDir, pythonDir);
+
+ if (matches)
+ {
+ client.SetStatus(McpStatus.Configured);
+ }
+ else if (attemptAutoRewrite)
+ {
+ // Attempt auto-rewrite if path mismatch detected
+ try
+ {
+ string rewriteResult = client.mcpType == McpTypes.Codex
+ ? McpConfigurationHelper.ConfigureCodexClient(pythonDir, configPath, client)
+ : McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client);
+
+ if (rewriteResult == "Configured successfully")
+ {
+ bool debugLogsEnabled = EditorPrefs.GetBool("MCPForUnity.DebugLogs", false);
+ if (debugLogsEnabled)
+ {
+ McpLog.Info($"Auto-updated MCP config for '{client.name}' to new path: {pythonDir}", always: false);
+ }
+ client.SetStatus(McpStatus.Configured);
+ }
+ else
+ {
+ client.SetStatus(McpStatus.IncorrectPath);
+ }
+ }
+ catch
+ {
+ client.SetStatus(McpStatus.IncorrectPath);
+ }
+ }
+ else
+ {
+ client.SetStatus(McpStatus.IncorrectPath);
+ }
+ }
+ else
+ {
+ client.SetStatus(McpStatus.MissingConfig);
+ }
+ }
+ catch (Exception ex)
+ {
+ client.SetStatus(McpStatus.Error, ex.Message);
+ }
+
+ return client.status != previousStatus;
+ }
+
+ public void RegisterClaudeCode()
+ {
+ var pathService = MCPServiceLocator.Paths;
+ string pythonDir = pathService.GetMcpServerPath();
+
+ if (string.IsNullOrEmpty(pythonDir))
+ {
+ throw new InvalidOperationException("Cannot register: Python directory not found");
+ }
+
+ string claudePath = pathService.GetClaudeCliPath();
+ if (string.IsNullOrEmpty(claudePath))
+ {
+ throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first.");
+ }
+
+ string uvPath = pathService.GetUvPath() ?? "uv";
+ string args = $"mcp add UnityMCP -- \"{uvPath}\" run --directory \"{pythonDir}\" server.py";
+ string projectDir = Path.GetDirectoryName(Application.dataPath);
+
+ string pathPrepend = null;
+ if (Application.platform == RuntimePlatform.OSXEditor)
+ {
+ pathPrepend = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
+ }
+ else if (Application.platform == RuntimePlatform.LinuxEditor)
+ {
+ pathPrepend = "/usr/local/bin:/usr/bin:/bin";
+ }
+
+ // Add the directory containing Claude CLI to PATH (for node/nvm scenarios)
+ try
+ {
+ string claudeDir = Path.GetDirectoryName(claudePath);
+ if (!string.IsNullOrEmpty(claudeDir))
+ {
+ pathPrepend = string.IsNullOrEmpty(pathPrepend)
+ ? claudeDir
+ : $"{claudeDir}:{pathPrepend}";
+ }
+ }
+ catch { }
+
+ if (!ExecPath.TryRun(claudePath, args, projectDir, out var stdout, out var stderr, 15000, pathPrepend))
+ {
+ string combined = ($"{stdout}\n{stderr}") ?? string.Empty;
+ if (combined.IndexOf("already exists", StringComparison.OrdinalIgnoreCase) >= 0)
+ {
+ Debug.Log("MCP-FOR-UNITY: MCP for Unity already registered with Claude Code.");
+ }
+ else
+ {
+ throw new InvalidOperationException($"Failed to register with Claude Code:\n{stderr}\n{stdout}");
+ }
+ return;
+ }
+
+ Debug.Log("MCP-FOR-UNITY: Successfully registered with Claude Code.");
+
+ // Update status
+ var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
+ if (claudeClient != null)
+ {
+ CheckClaudeCodeConfiguration(claudeClient);
+ }
+ }
+
+ public void UnregisterClaudeCode()
+ {
+ var pathService = MCPServiceLocator.Paths;
+ string claudePath = pathService.GetClaudeCliPath();
+
+ if (string.IsNullOrEmpty(claudePath))
+ {
+ throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first.");
+ }
+
+ string projectDir = Path.GetDirectoryName(Application.dataPath);
+ string pathPrepend = Application.platform == RuntimePlatform.OSXEditor
+ ? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
+ : null;
+
+ // Check if UnityMCP server exists (fixed - only check for "UnityMCP")
+ bool serverExists = ExecPath.TryRun(claudePath, "mcp get UnityMCP", projectDir, out _, out _, 7000, pathPrepend);
+
+ if (!serverExists)
+ {
+ // Nothing to unregister
+ var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
+ if (claudeClient != null)
+ {
+ claudeClient.SetStatus(McpStatus.NotConfigured);
+ }
+ Debug.Log("MCP-FOR-UNITY: No MCP for Unity server found - already unregistered.");
+ return;
+ }
+
+ // Remove the server
+ if (ExecPath.TryRun(claudePath, "mcp remove UnityMCP", projectDir, out var stdout, out var stderr, 10000, pathPrepend))
+ {
+ Debug.Log("MCP-FOR-UNITY: MCP server successfully unregistered from Claude Code.");
+ }
+ else
+ {
+ throw new InvalidOperationException($"Failed to unregister: {stderr}");
+ }
+
+ // Update status
+ var client = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
+ if (client != null)
+ {
+ client.SetStatus(McpStatus.NotConfigured);
+ CheckClaudeCodeConfiguration(client);
+ }
+ }
+
+ public string GetConfigPath(McpClient client)
+ {
+ // Claude Code is managed via CLI, not config files
+ if (client.mcpType == McpTypes.ClaudeCode)
+ {
+ return "Not applicable (managed via Claude CLI)";
+ }
+
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ return client.windowsConfigPath;
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ return client.macConfigPath;
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ return client.linuxConfigPath;
+
+ return "Unknown";
+ }
+
+ public string GenerateConfigJson(McpClient client)
+ {
+ string pythonDir = MCPServiceLocator.Paths.GetMcpServerPath();
+ string uvPath = MCPServiceLocator.Paths.GetUvPath();
+
+ // Claude Code uses CLI commands, not JSON config
+ if (client.mcpType == McpTypes.ClaudeCode)
+ {
+ if (string.IsNullOrEmpty(pythonDir) || string.IsNullOrEmpty(uvPath))
+ {
+ return "# Error: Configuration not available - check paths in Advanced Settings";
+ }
+
+ // Show the actual command that RegisterClaudeCode() uses
+ string registerCommand = $"claude mcp add UnityMCP -- \"{uvPath}\" run --directory \"{pythonDir}\" server.py";
+
+ return "# Register the MCP server with Claude Code:\n" +
+ $"{registerCommand}\n\n" +
+ "# Unregister the MCP server:\n" +
+ "claude mcp remove UnityMCP\n\n" +
+ "# List registered servers:\n" +
+ "claude mcp list # Only works when claude is run in the project's directory";
+ }
+
+ if (string.IsNullOrEmpty(pythonDir) || string.IsNullOrEmpty(uvPath))
+ return "{ \"error\": \"Configuration not available - check paths in Advanced Settings\" }";
+
+ try
+ {
+ if (client.mcpType == McpTypes.Codex)
+ {
+ return CodexConfigHelper.BuildCodexServerBlock(uvPath,
+ McpConfigFileHelper.ResolveServerDirectory(pythonDir, null));
+ }
+ else
+ {
+ return ConfigJsonBuilder.BuildManualConfigJson(uvPath, pythonDir, client);
+ }
+ }
+ catch (Exception ex)
+ {
+ return $"{{ \"error\": \"{ex.Message}\" }}";
+ }
+ }
+
+ public string GetInstallationSteps(McpClient client)
+ {
+ string baseSteps = client.mcpType switch
+ {
+ McpTypes.ClaudeDesktop =>
+ "1. Open Claude Desktop\n" +
+ "2. Go to Settings > Developer > Edit Config\n" +
+ " OR open the config file at the path above\n" +
+ "3. Paste the configuration JSON\n" +
+ "4. Save and restart Claude Desktop",
+
+ McpTypes.Cursor =>
+ "1. Open Cursor\n" +
+ "2. Go to File > Preferences > Cursor Settings > MCP > Add new global MCP server\n" +
+ " OR open the config file at the path above\n" +
+ "3. Paste the configuration JSON\n" +
+ "4. Save and restart Cursor",
+
+ McpTypes.Windsurf =>
+ "1. Open Windsurf\n" +
+ "2. Go to File > Preferences > Windsurf Settings > MCP > Manage MCPs > View raw config\n" +
+ " OR open the config file at the path above\n" +
+ "3. Paste the configuration JSON\n" +
+ "4. Save and restart Windsurf",
+
+ McpTypes.VSCode =>
+ "1. Ensure VSCode and GitHub Copilot extension are installed\n" +
+ "2. Open or create mcp.json at the path above\n" +
+ "3. Paste the configuration JSON\n" +
+ "4. Save and restart VSCode",
+
+ McpTypes.Kiro =>
+ "1. Open Kiro\n" +
+ "2. Go to File > Settings > Settings > Search for \"MCP\" > Open Workspace MCP Config\n" +
+ " OR open the config file at the path above\n" +
+ "3. Paste the configuration JSON\n" +
+ "4. Save and restart Kiro",
+
+ McpTypes.Codex =>
+ "1. Run 'codex config edit' in a terminal\n" +
+ " OR open the config file at the path above\n" +
+ "2. Paste the configuration TOML\n" +
+ "3. Save and restart Codex",
+
+ McpTypes.ClaudeCode =>
+ "1. Ensure Claude CLI is installed\n" +
+ "2. Use the Register button to register automatically\n" +
+ " OR manually run: claude mcp add UnityMCP\n" +
+ "3. Restart Claude Code",
+
+ _ => "Configuration steps not available for this client."
+ };
+
+ return baseSteps;
+ }
+
+ private void CheckClaudeCodeConfiguration(McpClient client)
+ {
+ try
+ {
+ string configPath = McpConfigurationHelper.GetClientConfigPath(client);
+
+ if (!File.Exists(configPath))
+ {
+ client.SetStatus(McpStatus.NotConfigured);
+ return;
+ }
+
+ string configJson = File.ReadAllText(configPath);
+ dynamic claudeConfig = JsonConvert.DeserializeObject(configJson);
+
+ if (claudeConfig?.mcpServers != null)
+ {
+ var servers = claudeConfig.mcpServers;
+ // Only check for UnityMCP (fixed - removed candidate hacks)
+ if (servers.UnityMCP != null)
+ {
+ client.SetStatus(McpStatus.Configured);
+ return;
+ }
+ }
+
+ client.SetStatus(McpStatus.NotConfigured);
+ }
+ catch (Exception ex)
+ {
+ client.SetStatus(McpStatus.Error, ex.Message);
+ }
+ }
+ }
+}
diff --git a/MCPForUnity/Editor/Services/ClientConfigurationService.cs.meta b/MCPForUnity/Editor/Services/ClientConfigurationService.cs.meta
new file mode 100644
index 00000000..ce6ade0b
--- /dev/null
+++ b/MCPForUnity/Editor/Services/ClientConfigurationService.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 76cad34d10fd24aaa95c4583c1f88fdf
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/MCPForUnity/Editor/Services/IBridgeControlService.cs b/MCPForUnity/Editor/Services/IBridgeControlService.cs
new file mode 100644
index 00000000..21952c37
--- /dev/null
+++ b/MCPForUnity/Editor/Services/IBridgeControlService.cs
@@ -0,0 +1,66 @@
+namespace MCPForUnity.Editor.Services
+{
+ ///
+ /// Service for controlling the Unity MCP Bridge connection
+ ///
+ public interface IBridgeControlService
+ {
+ ///
+ /// Gets whether the bridge is currently running
+ ///
+ bool IsRunning { get; }
+
+ ///
+ /// Gets the current port the bridge is listening on
+ ///
+ int CurrentPort { get; }
+
+ ///
+ /// Gets whether the bridge is in auto-connect mode
+ ///
+ bool IsAutoConnectMode { get; }
+
+ ///
+ /// Starts the Unity MCP Bridge
+ ///
+ void Start();
+
+ ///
+ /// Stops the Unity MCP Bridge
+ ///
+ void Stop();
+
+ ///
+ /// Verifies the bridge connection by sending a ping and waiting for a pong response
+ ///
+ /// The port to verify
+ /// Verification result with detailed status
+ BridgeVerificationResult Verify(int port);
+ }
+
+ ///
+ /// Result of a bridge verification attempt
+ ///
+ public class BridgeVerificationResult
+ {
+ ///
+ /// Whether the verification was successful
+ ///
+ public bool Success { get; set; }
+
+ ///
+ /// Human-readable message about the verification result
+ ///
+ public string Message { get; set; }
+
+ ///
+ /// Whether the handshake was valid (FRAMING=1 protocol)
+ ///
+ public bool HandshakeValid { get; set; }
+
+ ///
+ /// Whether the ping/pong exchange succeeded
+ ///
+ public bool PingSucceeded { get; set; }
+ }
+}
diff --git a/MCPForUnity/Editor/Services/IBridgeControlService.cs.meta b/MCPForUnity/Editor/Services/IBridgeControlService.cs.meta
new file mode 100644
index 00000000..ec50da3d
--- /dev/null
+++ b/MCPForUnity/Editor/Services/IBridgeControlService.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 6b5d9f677f6f54fc59e6fe921b260c61
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/MCPForUnity/Editor/Services/IClientConfigurationService.cs b/MCPForUnity/Editor/Services/IClientConfigurationService.cs
new file mode 100644
index 00000000..07af087b
--- /dev/null
+++ b/MCPForUnity/Editor/Services/IClientConfigurationService.cs
@@ -0,0 +1,95 @@
+using MCPForUnity.Editor.Models;
+
+namespace MCPForUnity.Editor.Services
+{
+ ///
+ /// Service for configuring MCP clients
+ ///
+ public interface IClientConfigurationService
+ {
+ ///
+ /// Configures a specific MCP client
+ ///
+ /// The client to configure
+ void ConfigureClient(McpClient client);
+
+ ///
+ /// Configures all detected/installed MCP clients (skips clients where CLI/tools not found)
+ ///
+ /// Summary of configuration results
+ ClientConfigurationSummary ConfigureAllDetectedClients();
+
+ ///
+ /// Checks the configuration status of a client
+ ///
+ /// The client to check
+ /// If true, attempts to auto-fix mismatched paths
+ /// True if status changed, false otherwise
+ bool CheckClientStatus(McpClient client, bool attemptAutoRewrite = true);
+
+ ///
+ /// Registers Unity MCP with Claude Code CLI
+ ///
+ void RegisterClaudeCode();
+
+ ///
+ /// Unregisters Unity MCP from Claude Code CLI
+ ///
+ void UnregisterClaudeCode();
+
+ ///
+ /// Gets the configuration file path for a client
+ ///
+ /// The client
+ /// Platform-specific config path
+ string GetConfigPath(McpClient client);
+
+ ///
+ /// Generates the configuration JSON for a client
+ ///
+ /// The client
+ /// JSON configuration string
+ string GenerateConfigJson(McpClient client);
+
+ ///
+ /// Gets human-readable installation steps for a client
+ ///
+ /// The client
+ /// Installation instructions
+ string GetInstallationSteps(McpClient client);
+ }
+
+ ///
+ /// Summary of configuration results for multiple clients
+ ///
+ public class ClientConfigurationSummary
+ {
+ ///
+ /// Number of clients successfully configured
+ ///
+ public int SuccessCount { get; set; }
+
+ ///
+ /// Number of clients that failed to configure
+ ///
+ public int FailureCount { get; set; }
+
+ ///
+ /// Number of clients skipped (already configured or tool not found)
+ ///
+ public int SkippedCount { get; set; }
+
+ ///
+ /// Detailed messages for each client
+ ///
+ public System.Collections.Generic.List Messages { get; set; } = new();
+
+ ///
+ /// Gets a human-readable summary message
+ ///
+ public string GetSummaryMessage()
+ {
+ return $"✓ {SuccessCount} configured, ⚠ {FailureCount} failed, ➜ {SkippedCount} skipped";
+ }
+ }
+}
diff --git a/MCPForUnity/Editor/Services/IClientConfigurationService.cs.meta b/MCPForUnity/Editor/Services/IClientConfigurationService.cs.meta
new file mode 100644
index 00000000..5110b69f
--- /dev/null
+++ b/MCPForUnity/Editor/Services/IClientConfigurationService.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: aae139cfae7ac4044ac52e2658005ea1
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/MCPForUnity/Editor/Services/IPathResolverService.cs b/MCPForUnity/Editor/Services/IPathResolverService.cs
new file mode 100644
index 00000000..9968af65
--- /dev/null
+++ b/MCPForUnity/Editor/Services/IPathResolverService.cs
@@ -0,0 +1,92 @@
+namespace MCPForUnity.Editor.Services
+{
+ ///
+ /// Service for resolving paths to required tools and supporting user overrides
+ ///
+ public interface IPathResolverService
+ {
+ ///
+ /// Gets the MCP server path (respects override if set)
+ ///
+ /// Path to the MCP server directory containing server.py, or null if not found
+ string GetMcpServerPath();
+
+ ///
+ /// Gets the UV package manager path (respects override if set)
+ ///
+ /// Path to the uv executable, or null if not found
+ string GetUvPath();
+
+ ///
+ /// Gets the Claude CLI path (respects override if set)
+ ///
+ /// Path to the claude executable, or null if not found
+ string GetClaudeCliPath();
+
+ ///
+ /// Checks if Python is detected on the system
+ ///
+ /// True if Python is found
+ bool IsPythonDetected();
+
+ ///
+ /// Checks if UV is detected on the system
+ ///
+ /// True if UV is found
+ bool IsUvDetected();
+
+ ///
+ /// Checks if Claude CLI is detected on the system
+ ///
+ /// True if Claude CLI is found
+ bool IsClaudeCliDetected();
+
+ ///
+ /// Sets an override for the MCP server path
+ ///
+ /// Path to override with
+ void SetMcpServerOverride(string path);
+
+ ///
+ /// Sets an override for the UV path
+ ///
+ /// Path to override with
+ void SetUvPathOverride(string path);
+
+ ///
+ /// Sets an override for the Claude CLI path
+ ///
+ /// Path to override with
+ void SetClaudeCliPathOverride(string path);
+
+ ///
+ /// Clears the MCP server path override
+ ///
+ void ClearMcpServerOverride();
+
+ ///
+ /// Clears the UV path override
+ ///
+ void ClearUvPathOverride();
+
+ ///
+ /// Clears the Claude CLI path override
+ ///
+ void ClearClaudeCliPathOverride();
+
+ ///
+ /// Gets whether a MCP server path override is active
+ ///
+ bool HasMcpServerOverride { get; }
+
+ ///
+ /// Gets whether a UV path override is active
+ ///
+ bool HasUvPathOverride { get; }
+
+ ///
+ /// Gets whether a Claude CLI path override is active
+ ///
+ bool HasClaudeCliPathOverride { get; }
+ }
+}
diff --git a/MCPForUnity/Editor/Services/IPathResolverService.cs.meta b/MCPForUnity/Editor/Services/IPathResolverService.cs.meta
new file mode 100644
index 00000000..75634531
--- /dev/null
+++ b/MCPForUnity/Editor/Services/IPathResolverService.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 1e8d388be507345aeb0eaf27fbd3c022
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/MCPForUnity/Editor/Services/MCPServiceLocator.cs b/MCPForUnity/Editor/Services/MCPServiceLocator.cs
new file mode 100644
index 00000000..547f9c23
--- /dev/null
+++ b/MCPForUnity/Editor/Services/MCPServiceLocator.cs
@@ -0,0 +1,52 @@
+namespace MCPForUnity.Editor.Services
+{
+ ///
+ /// Service locator for accessing MCP services without dependency injection
+ ///
+ public static class MCPServiceLocator
+ {
+ private static IBridgeControlService _bridgeService;
+ private static IClientConfigurationService _clientService;
+ private static IPathResolverService _pathService;
+
+ ///
+ /// Gets the bridge control service
+ ///
+ public static IBridgeControlService Bridge => _bridgeService ??= new BridgeControlService();
+
+ ///
+ /// Gets the client configuration service
+ ///
+ public static IClientConfigurationService Client => _clientService ??= new ClientConfigurationService();
+
+ ///
+ /// Gets the path resolver service
+ ///
+ public static IPathResolverService Paths => _pathService ??= new PathResolverService();
+
+ ///
+ /// Registers a custom implementation for a service (useful for testing)
+ ///
+ /// The service interface type
+ /// The implementation to register
+ public static void Register(T implementation) where T : class
+ {
+ if (implementation is IBridgeControlService b)
+ _bridgeService = b;
+ else if (implementation is IClientConfigurationService c)
+ _clientService = c;
+ else if (implementation is IPathResolverService p)
+ _pathService = p;
+ }
+
+ ///
+ /// Resets all services to their default implementations (useful for testing)
+ ///
+ public static void Reset()
+ {
+ _bridgeService = null;
+ _clientService = null;
+ _pathService = null;
+ }
+ }
+}
diff --git a/MCPForUnity/Editor/Services/MCPServiceLocator.cs.meta b/MCPForUnity/Editor/Services/MCPServiceLocator.cs.meta
new file mode 100644
index 00000000..cd9b4021
--- /dev/null
+++ b/MCPForUnity/Editor/Services/MCPServiceLocator.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 276d6a9f9a1714ead91573945de78992
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/MCPForUnity/Editor/Services/PathResolverService.cs b/MCPForUnity/Editor/Services/PathResolverService.cs
new file mode 100644
index 00000000..6d239cc0
--- /dev/null
+++ b/MCPForUnity/Editor/Services/PathResolverService.cs
@@ -0,0 +1,242 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using MCPForUnity.Editor.Helpers;
+using UnityEditor;
+using UnityEngine;
+
+namespace MCPForUnity.Editor.Services
+{
+ ///
+ /// Implementation of path resolver service with override support
+ ///
+ public class PathResolverService : IPathResolverService
+ {
+ private const string PythonDirOverrideKey = "MCPForUnity.PythonDirOverride";
+ private const string UvPathOverrideKey = "MCPForUnity.UvPath";
+ private const string ClaudeCliPathOverrideKey = "MCPForUnity.ClaudeCliPath";
+
+ public bool HasMcpServerOverride => !string.IsNullOrEmpty(EditorPrefs.GetString(PythonDirOverrideKey, null));
+ public bool HasUvPathOverride => !string.IsNullOrEmpty(EditorPrefs.GetString(UvPathOverrideKey, null));
+ public bool HasClaudeCliPathOverride => !string.IsNullOrEmpty(EditorPrefs.GetString(ClaudeCliPathOverrideKey, null));
+
+ public string GetMcpServerPath()
+ {
+ // Check for override first
+ string overridePath = EditorPrefs.GetString(PythonDirOverrideKey, null);
+ if (!string.IsNullOrEmpty(overridePath) && File.Exists(Path.Combine(overridePath, "server.py")))
+ {
+ return overridePath;
+ }
+
+ // Fall back to automatic detection
+ return McpPathResolver.FindPackagePythonDirectory(false);
+ }
+
+ public string GetUvPath()
+ {
+ // Check for override first
+ string overridePath = EditorPrefs.GetString(UvPathOverrideKey, null);
+ if (!string.IsNullOrEmpty(overridePath) && File.Exists(overridePath))
+ {
+ return overridePath;
+ }
+
+ // Fall back to automatic detection
+ try
+ {
+ return ServerInstaller.FindUvPath();
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ public string GetClaudeCliPath()
+ {
+ // Check for override first
+ string overridePath = EditorPrefs.GetString(ClaudeCliPathOverrideKey, null);
+ if (!string.IsNullOrEmpty(overridePath) && File.Exists(overridePath))
+ {
+ return overridePath;
+ }
+
+ // Fall back to automatic detection
+ return ExecPath.ResolveClaude();
+ }
+
+ public bool IsPythonDetected()
+ {
+ try
+ {
+ // Windows-specific Python detection
+ if (Application.platform == RuntimePlatform.WindowsEditor)
+ {
+ // Common Windows Python installation paths
+ string[] windowsCandidates =
+ {
+ @"C:\Python313\python.exe",
+ @"C:\Python312\python.exe",
+ @"C:\Python311\python.exe",
+ @"C:\Python310\python.exe",
+ @"C:\Python39\python.exe",
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python313\python.exe"),
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python312\python.exe"),
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python311\python.exe"),
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python310\python.exe"),
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python39\python.exe"),
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python313\python.exe"),
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python312\python.exe"),
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python311\python.exe"),
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python310\python.exe"),
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python39\python.exe"),
+ };
+
+ foreach (string c in windowsCandidates)
+ {
+ if (File.Exists(c)) return true;
+ }
+
+ // Try 'where python' command (Windows equivalent of 'which')
+ var psi = new ProcessStartInfo
+ {
+ FileName = "where",
+ Arguments = "python",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true
+ };
+ using (var p = Process.Start(psi))
+ {
+ string outp = p.StandardOutput.ReadToEnd().Trim();
+ p.WaitForExit(2000);
+ if (p.ExitCode == 0 && !string.IsNullOrEmpty(outp))
+ {
+ string[] lines = outp.Split('\n');
+ foreach (string line in lines)
+ {
+ string trimmed = line.Trim();
+ if (File.Exists(trimmed)) return true;
+ }
+ }
+ }
+ }
+ else
+ {
+ // macOS/Linux detection
+ string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
+ string[] candidates =
+ {
+ "/opt/homebrew/bin/python3",
+ "/usr/local/bin/python3",
+ "/usr/bin/python3",
+ "/opt/local/bin/python3",
+ Path.Combine(home, ".local", "bin", "python3"),
+ "/Library/Frameworks/Python.framework/Versions/3.13/bin/python3",
+ "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3",
+ };
+ foreach (string c in candidates)
+ {
+ if (File.Exists(c)) return true;
+ }
+
+ // Try 'which python3'
+ var psi = new ProcessStartInfo
+ {
+ FileName = "/usr/bin/which",
+ Arguments = "python3",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true
+ };
+ using (var p = Process.Start(psi))
+ {
+ string outp = p.StandardOutput.ReadToEnd().Trim();
+ p.WaitForExit(2000);
+ if (p.ExitCode == 0 && !string.IsNullOrEmpty(outp) && File.Exists(outp)) return true;
+ }
+ }
+ }
+ catch { }
+ return false;
+ }
+
+ public bool IsUvDetected()
+ {
+ return !string.IsNullOrEmpty(GetUvPath());
+ }
+
+ public bool IsClaudeCliDetected()
+ {
+ return !string.IsNullOrEmpty(GetClaudeCliPath());
+ }
+
+ public void SetMcpServerOverride(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ ClearMcpServerOverride();
+ return;
+ }
+
+ if (!File.Exists(Path.Combine(path, "server.py")))
+ {
+ throw new ArgumentException("The selected folder does not contain server.py");
+ }
+
+ EditorPrefs.SetString(PythonDirOverrideKey, path);
+ }
+
+ public void SetUvPathOverride(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ ClearUvPathOverride();
+ return;
+ }
+
+ if (!File.Exists(path))
+ {
+ throw new ArgumentException("The selected UV executable does not exist");
+ }
+
+ EditorPrefs.SetString(UvPathOverrideKey, path);
+ }
+
+ public void SetClaudeCliPathOverride(string path)
+ {
+ if (string.IsNullOrEmpty(path))
+ {
+ ClearClaudeCliPathOverride();
+ return;
+ }
+
+ if (!File.Exists(path))
+ {
+ throw new ArgumentException("The selected Claude CLI executable does not exist");
+ }
+
+ EditorPrefs.SetString(ClaudeCliPathOverrideKey, path);
+ // Also update the ExecPath helper for backwards compatibility
+ ExecPath.SetClaudeCliPath(path);
+ }
+
+ public void ClearMcpServerOverride()
+ {
+ EditorPrefs.DeleteKey(PythonDirOverrideKey);
+ }
+
+ public void ClearUvPathOverride()
+ {
+ EditorPrefs.DeleteKey(UvPathOverrideKey);
+ }
+
+ public void ClearClaudeCliPathOverride()
+ {
+ EditorPrefs.DeleteKey(ClaudeCliPathOverrideKey);
+ }
+ }
+}
diff --git a/MCPForUnity/Editor/Services/PathResolverService.cs.meta b/MCPForUnity/Editor/Services/PathResolverService.cs.meta
new file mode 100644
index 00000000..09dbc1ec
--- /dev/null
+++ b/MCPForUnity/Editor/Services/PathResolverService.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 00a6188fd15a847fa8cc7cb7a4ce3dce
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/MCPForUnity/Editor/Setup/SetupWizard.cs b/MCPForUnity/Editor/Setup/SetupWizard.cs
index a97926ea..691e482f 100644
--- a/MCPForUnity/Editor/Setup/SetupWizard.cs
+++ b/MCPForUnity/Editor/Setup/SetupWizard.cs
@@ -141,8 +141,17 @@ public static void CheckDependencies()
///
/// Open MCP Client Configuration window
///
- [MenuItem("Window/MCP For Unity/Open MCP Window", priority = 4)]
+ [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/Tools/ManageScript.cs b/MCPForUnity/Editor/Tools/ManageScript.cs
index 2d970486..51669c65 100644
--- a/MCPForUnity/Editor/Tools/ManageScript.cs
+++ b/MCPForUnity/Editor/Tools/ManageScript.cs
@@ -2551,111 +2551,111 @@ private static void ValidateSemanticRules(string contents, System.Collections.Ge
// }
// }
}
-}
-
-// Debounced refresh/compile scheduler to coalesce bursts of edits
-static class RefreshDebounce
-{
- private static int _pending;
- private static readonly object _lock = new object();
- private static readonly HashSet _paths = new HashSet(StringComparer.OrdinalIgnoreCase);
-
- // The timestamp of the most recent schedule request.
- private static DateTime _lastRequest;
-
- // Guard to ensure we only have a single ticking callback running.
- private static bool _scheduled;
- public static void Schedule(string relPath, TimeSpan window)
+ // Debounced refresh/compile scheduler to coalesce bursts of edits
+ static class RefreshDebounce
{
- // Record that work is pending and track the path in a threadsafe way.
- Interlocked.Exchange(ref _pending, 1);
- lock (_lock)
- {
- _paths.Add(relPath);
- _lastRequest = DateTime.UtcNow;
+ private static int _pending;
+ private static readonly object _lock = new object();
+ private static readonly HashSet _paths = new HashSet(StringComparer.OrdinalIgnoreCase);
- // If a debounce timer is already scheduled it will pick up the new request.
- if (_scheduled)
- return;
+ // The timestamp of the most recent schedule request.
+ private static DateTime _lastRequest;
- _scheduled = true;
- }
-
- // Kick off a ticking callback that waits until the window has elapsed
- // from the last request before performing the refresh.
- EditorApplication.delayCall += () => Tick(window);
- // Nudge the editor loop so ticks run even if the window is unfocused
- EditorApplication.QueuePlayerLoopUpdate();
- }
+ // Guard to ensure we only have a single ticking callback running.
+ private static bool _scheduled;
- private static void Tick(TimeSpan window)
- {
- bool ready;
- lock (_lock)
+ public static void Schedule(string relPath, TimeSpan window)
{
- // Only proceed once the debounce window has fully elapsed.
- ready = (DateTime.UtcNow - _lastRequest) >= window;
- if (ready)
+ // Record that work is pending and track the path in a threadsafe way.
+ Interlocked.Exchange(ref _pending, 1);
+ lock (_lock)
{
- _scheduled = false;
+ _paths.Add(relPath);
+ _lastRequest = DateTime.UtcNow;
+
+ // If a debounce timer is already scheduled it will pick up the new request.
+ if (_scheduled)
+ return;
+
+ _scheduled = true;
}
- }
- if (!ready)
- {
- // Window has not yet elapsed; check again on the next editor tick.
+ // Kick off a ticking callback that waits until the window has elapsed
+ // from the last request before performing the refresh.
EditorApplication.delayCall += () => Tick(window);
- return;
+ // Nudge the editor loop so ticks run even if the window is unfocused
+ EditorApplication.QueuePlayerLoopUpdate();
}
- if (Interlocked.Exchange(ref _pending, 0) == 1)
+ private static void Tick(TimeSpan window)
{
- string[] toImport;
- lock (_lock) { toImport = _paths.ToArray(); _paths.Clear(); }
- foreach (var p in toImport)
+ bool ready;
+ lock (_lock)
+ {
+ // Only proceed once the debounce window has fully elapsed.
+ ready = (DateTime.UtcNow - _lastRequest) >= window;
+ if (ready)
+ {
+ _scheduled = false;
+ }
+ }
+
+ if (!ready)
{
- var sp = ManageScriptRefreshHelpers.SanitizeAssetsPath(p);
- AssetDatabase.ImportAsset(sp, ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport);
+ // Window has not yet elapsed; check again on the next editor tick.
+ EditorApplication.delayCall += () => Tick(window);
+ return;
}
+
+ if (Interlocked.Exchange(ref _pending, 0) == 1)
+ {
+ string[] toImport;
+ lock (_lock) { toImport = _paths.ToArray(); _paths.Clear(); }
+ foreach (var p in toImport)
+ {
+ var sp = ManageScriptRefreshHelpers.SanitizeAssetsPath(p);
+ AssetDatabase.ImportAsset(sp, ImportAssetOptions.ForceUpdate | ImportAssetOptions.ForceSynchronousImport);
+ }
#if UNITY_EDITOR
- UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation();
+ UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation();
#endif
- // Fallback if needed:
- // AssetDatabase.Refresh();
+ // Fallback if needed:
+ // AssetDatabase.Refresh();
+ }
}
}
-}
-static class ManageScriptRefreshHelpers
-{
- public static string SanitizeAssetsPath(string p)
+ static class ManageScriptRefreshHelpers
{
- if (string.IsNullOrEmpty(p)) return p;
- p = p.Replace('\\', '/').Trim();
- if (p.StartsWith("unity://path/", StringComparison.OrdinalIgnoreCase))
- p = p.Substring("unity://path/".Length);
- while (p.StartsWith("Assets/Assets/", StringComparison.OrdinalIgnoreCase))
- p = p.Substring("Assets/".Length);
- if (!p.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
- p = "Assets/" + p.TrimStart('/');
- return p;
- }
+ public static string SanitizeAssetsPath(string p)
+ {
+ if (string.IsNullOrEmpty(p)) return p;
+ p = p.Replace('\\', '/').Trim();
+ if (p.StartsWith("unity://path/", StringComparison.OrdinalIgnoreCase))
+ p = p.Substring("unity://path/".Length);
+ while (p.StartsWith("Assets/Assets/", StringComparison.OrdinalIgnoreCase))
+ p = p.Substring("Assets/".Length);
+ if (!p.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
+ p = "Assets/" + p.TrimStart('/');
+ return p;
+ }
- public static void ScheduleScriptRefresh(string relPath)
- {
- var sp = SanitizeAssetsPath(relPath);
- RefreshDebounce.Schedule(sp, TimeSpan.FromMilliseconds(200));
- }
+ public static void ScheduleScriptRefresh(string relPath)
+ {
+ var sp = SanitizeAssetsPath(relPath);
+ RefreshDebounce.Schedule(sp, TimeSpan.FromMilliseconds(200));
+ }
- public static void ImportAndRequestCompile(string relPath, bool synchronous = true)
- {
- var sp = SanitizeAssetsPath(relPath);
- var opts = ImportAssetOptions.ForceUpdate;
- if (synchronous) opts |= ImportAssetOptions.ForceSynchronousImport;
- AssetDatabase.ImportAsset(sp, opts);
+ public static void ImportAndRequestCompile(string relPath, bool synchronous = true)
+ {
+ var sp = SanitizeAssetsPath(relPath);
+ var opts = ImportAssetOptions.ForceUpdate;
+ if (synchronous) opts |= ImportAssetOptions.ForceSynchronousImport;
+ AssetDatabase.ImportAsset(sp, opts);
#if UNITY_EDITOR
- UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation();
+ UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation();
#endif
+ }
}
}
diff --git a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindowNew.cs b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindowNew.cs
new file mode 100644
index 00000000..3987f112
--- /dev/null
+++ b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindowNew.cs
@@ -0,0 +1,834 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+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 MCPForUnityEditorWindowNew : EditorWindow
+ {
+ // Protocol enum for future HTTP support
+ private enum ConnectionProtocol
+ {
+ Stdio,
+ // HTTPStreaming // Future
+ }
+
+ // Settings UI Elements
+ 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 int selectedClientIndex = 0;
+ private ValidationLevel currentValidationLevel = ValidationLevel.Standard;
+
+ // Validation levels matching the existing enum
+ private enum ValidationLevel
+ {
+ Basic,
+ Standard,
+ Comprehensive,
+ Strict
+ }
+
+ public static void ShowWindow()
+ {
+ var window = GetWindow("MCP For Unity");
+ window.minSize = new Vector2(500, 600);
+ }
+ public void CreateGUI()
+ {
+ // Determine base path (Package Manager vs Asset Store install)
+ string basePath = AssetPathUtility.GetMcpPackageRootPath();
+
+ // Load UXML
+ var visualTree = AssetDatabase.LoadAssetAtPath(
+ $"{basePath}/Editor/Windows/MCPForUnityEditorWindowNew.uxml"
+ );
+
+ if (visualTree == null)
+ {
+ McpLog.Error($"Failed to load UXML at: {basePath}/Editor/Windows/MCPForUnityEditorWindowNew.uxml");
+ return;
+ }
+
+ visualTree.CloneTree(rootVisualElement);
+
+ // Load USS
+ var styleSheet = AssetDatabase.LoadAssetAtPath(
+ $"{basePath}/Editor/Windows/MCPForUnityEditorWindowNew.uss"
+ );
+
+ if (styleSheet != null)
+ {
+ rootVisualElement.styleSheets.Add(styleSheet);
+ }
+
+ // 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 void OnEnable()
+ {
+ EditorApplication.update += OnEditorUpdate;
+ }
+
+ private void OnDisable()
+ {
+ EditorApplication.update -= OnEditorUpdate;
+ }
+
+ private void OnFocus()
+ {
+ // Only refresh data if UI is built
+ if (rootVisualElement == null || rootVisualElement.childCount == 0)
+ return;
+
+ RefreshAllData();
+ }
+
+ private void OnEditorUpdate()
+ {
+ // Only update UI if it's built
+ if (rootVisualElement == null || rootVisualElement.childCount == 0)
+ return;
+
+ UpdateConnectionStatus();
+ }
+
+ private void RefreshAllData()
+ {
+ // Update connection status
+ UpdateConnectionStatus();
+
+ // Auto-verify bridge health if connected
+ if (MCPServiceLocator.Bridge.IsRunning)
+ {
+ VerifyBridgeConnection();
+ }
+
+ // Update path overrides
+ UpdatePathOverrides();
+
+ // Refresh selected client (may have been configured externally)
+ if (selectedClientIndex >= 0 && selectedClientIndex < mcpClients.clients.Count)
+ {
+ var client = mcpClients.clients[selectedClientIndex];
+ MCPServiceLocator.Client.CheckClientStatus(client);
+ UpdateClientStatus();
+ UpdateManualConfiguration();
+ UpdateClaudeCliPathVisibility();
+ }
+ }
+
+ private void CacheUIElements()
+ {
+ // Settings
+ debugLogsToggle = rootVisualElement.Q("debug-logs-toggle");
+ validationLevelField = rootVisualElement.Q("validation-level");
+ validationDescription = rootVisualElement.Q