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 UnityEngine; using MCPForUnity.Editor.Data; using MCPForUnity.Editor.Helpers; using MCPForUnity.Editor.Models; 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) 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; public static void ShowWindow() { GetWindow("MCP For Unity"); } private void OnEnable() { UpdatePythonServerInstallationStatus(); // 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) { CheckMcpConfiguration(mcpClient); } // Load validation level setting LoadValidationLevelSetting(); // First-run auto-setup only if Claude CLI is available if (autoRegisterEnabled && !string.IsNullOrEmpty(ExecPath.ResolveClaude())) { AutoFirstRunSetup(); } } 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) { McpClient selectedClient = mcpClients.clients[selectedClientIndex]; CheckMcpConfiguration(selectedClient); } Repaint(); } private Color GetStatusColor(McpStatus status) { // 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 }; } private void UpdatePythonServerInstallationStatus() { 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; } } private void DrawStatusDot(Rect statusRect, Color statusColor, float size = 12) { 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); // 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); } private void OnGUI() { 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(); EditorGUILayout.Space(horizontalSpacing); EditorGUILayout.BeginVertical(GUILayout.Width(colWidth), GUILayout.Height(topPanelHeight)); DrawBridgeSection(); EditorGUILayout.EndVertical(); } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(10); // Second row: MCP Client Configuration (left) and Script Validation (right) EditorGUILayout.BeginHorizontal(); { 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(); } EditorGUILayout.EndHorizontal(); // Minimal bottom padding EditorGUILayout.Space(2); EditorGUILayout.EndScrollView(); } private void DrawHeader() { EditorGUILayout.Space(15); Rect titleRect = EditorGUILayout.GetControlRect(false, 40); EditorGUI.DrawRect(titleRect, new Color(0.2f, 0.2f, 0.2f, 0.1f)); GUIStyle titleStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 16, alignment = TextAnchor.MiddleLeft }; GUI.Label( new Rect(titleRect.x + 15, titleRect.y + 8, titleRect.width - 30, titleRect.height), "MCP For Unity", titleStyle ); // Place the Show Debug Logs toggle on the same header row, right-aligned float toggleWidth = 160f; Rect toggleRect = new Rect(titleRect.xMax - toggleWidth - 12f, titleRect.y + 10f, toggleWidth, 20f); bool newDebug = GUI.Toggle(toggleRect, debugLogsEnabled, "Show Debug Logs"); if (newDebug != debugLogsEnabled) { debugLogsEnabled = newDebug; EditorPrefs.SetBool("MCPForUnity.DebugLogs", debugLogsEnabled); if (debugLogsEnabled) { LogDebugPrefsState(); } } EditorGUILayout.Space(15); } private void LogDebugPrefsState() { try { string pythonDirOverridePref = SafeGetPrefString("MCPForUnity.PythonDirOverride"); string uvPathPref = SafeGetPrefString("MCPForUnity.UvPath"); string serverSrcPref = SafeGetPrefString("MCPForUnity.ServerSrc"); bool useEmbedded = SafeGetPrefBool("MCPForUnity.UseEmbeddedServer"); // Version-scoped detection key string embeddedVer = ReadEmbeddedVersionOrFallback(); string detectKey = $"MCPForUnity.LegacyDetectLogged:{embeddedVer}"; bool detectLogged = SafeGetPrefBool(detectKey); // Project-scoped auto-register key string projectPath = Application.dataPath ?? string.Empty; string autoKey = $"MCPForUnity.AutoRegistered.{ComputeSha1(projectPath)}"; bool autoRegistered = SafeGetPrefBool(autoKey); MCPForUnity.Editor.Helpers.McpLog.Info( "MCP Debug Prefs:\n" + $" DebugLogs: {debugLogsEnabled}\n" + $" PythonDirOverride: '{pythonDirOverridePref}'\n" + $" UvPath: '{uvPathPref}'\n" + $" ServerSrc: '{serverSrcPref}'\n" + $" UseEmbeddedServer: {useEmbedded}\n" + $" DetectOnceKey: '{detectKey}' => {detectLogged}\n" + $" AutoRegisteredKey: '{autoKey}' => {autoRegistered}", always: false ); } catch (Exception ex) { UnityEngine.Debug.LogWarning($"MCP Debug Prefs logging failed: {ex.Message}"); } } private static string SafeGetPrefString(string key) { try { return EditorPrefs.GetString(key, string.Empty) ?? string.Empty; } catch { return string.Empty; } } private static bool SafeGetPrefBool(string key) { try { return EditorPrefs.GetBool(key, false); } catch { return false; } } private static string ReadEmbeddedVersionOrFallback() { try { if (ServerPathResolver.TryFindEmbeddedServerSource(out var embeddedSrc)) { var p = Path.Combine(embeddedSrc, "server_version.txt"); if (File.Exists(p)) { var s = File.ReadAllText(p)?.Trim(); if (!string.IsNullOrEmpty(s)) return s; } } } catch { } return "unknown"; } private void DrawServerStatusSection() { EditorGUILayout.BeginVertical(EditorStyles.helpBox); GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 14 }; EditorGUILayout.LabelField("Server Status", sectionTitleStyle); EditorGUILayout.Space(8); EditorGUILayout.BeginHorizontal(); Rect statusRect = GUILayoutUtility.GetRect(0, 28, GUILayout.Width(24)); DrawStatusDot(statusRect, pythonServerInstallationStatusColor, 16); GUIStyle statusStyle = new GUIStyle(EditorStyles.label) { fontSize = 12, fontStyle = FontStyle.Bold }; EditorGUILayout.LabelField(pythonServerInstallationStatus, statusStyle, GUILayout.Height(28)); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(5); EditorGUILayout.BeginHorizontal(); bool isAutoMode = MCPForUnityBridge.IsAutoConnectMode(); GUIStyle modeStyle = new GUIStyle(EditorStyles.miniLabel) { fontSize = 11 }; EditorGUILayout.LabelField($"Mode: {(isAutoMode ? "Auto" : "Standard")}", modeStyle); GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); int currentUnityPort = MCPForUnityBridge.GetCurrentPort(); GUIStyle portStyle = new GUIStyle(EditorStyles.miniLabel) { fontSize = 11 }; EditorGUILayout.LabelField($"Ports: Unity {currentUnityPort}, MCP {mcpPort}", portStyle); EditorGUILayout.Space(5); /// Auto-Setup button below ports string setupButtonText = (lastClientRegisteredOk && lastBridgeVerifiedOk) ? "Connected ✓" : "Auto-Setup"; if (GUILayout.Button(setupButtonText, GUILayout.Height(24))) { RunSetupNow(); } EditorGUILayout.Space(4); // Rebuild MCP Server button with tooltip tag using (new EditorGUILayout.HorizontalScope()) { GUILayout.FlexibleSpace(); GUIContent repairLabel = new GUIContent( "Rebuild MCP Server", "Deletes the installed server and re-copies it from the package. Use this to update the server after making source code changes or if the installation is corrupted." ); if (GUILayout.Button(repairLabel, GUILayout.Width(160), GUILayout.Height(22))) { bool ok = global::MCPForUnity.Editor.Helpers.ServerInstaller.RebuildMcpServer(); if (ok) { EditorUtility.DisplayDialog("MCP For Unity", "Server rebuilt successfully.", "OK"); UpdatePythonServerInstallationStatus(); } else { EditorUtility.DisplayDialog("MCP For Unity", "Rebuild failed. Please check Console for details.", "OK"); } } } // (Removed descriptive tool tag under the Repair button) // (Show Debug Logs toggle moved to header) EditorGUILayout.Space(2); // Python detection warning with link if (!IsPythonDetected()) { GUIStyle warnStyle = new GUIStyle(EditorStyles.label) { richText = true, wordWrap = true }; EditorGUILayout.LabelField("Warning: No Python installation found.", warnStyle); using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button("Open Install Instructions", GUILayout.Width(200))) { Application.OpenURL("https://www.python.org/downloads/"); } } EditorGUILayout.Space(4); } // Troubleshooting helpers if (pythonServerInstallationStatusColor != Color.green) { using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button("Select server folder…", GUILayout.Width(160))) { string picked = EditorUtility.OpenFolderPanel("Select UnityMcpServer/src", Application.dataPath, ""); if (!string.IsNullOrEmpty(picked) && File.Exists(Path.Combine(picked, "server.py"))) { pythonDirOverride = picked; EditorPrefs.SetString("MCPForUnity.PythonDirOverride", pythonDirOverride); UpdatePythonServerInstallationStatus(); } else if (!string.IsNullOrEmpty(picked)) { EditorUtility.DisplayDialog("Invalid Selection", "The selected folder does not contain server.py", "OK"); } } if (GUILayout.Button("Verify again", GUILayout.Width(120))) { UpdatePythonServerInstallationStatus(); } } } EditorGUILayout.EndVertical(); } private void DrawBridgeSection() { EditorGUILayout.BeginVertical(EditorStyles.helpBox); // Always reflect the live state each repaint to avoid stale UI after recompiles isUnityBridgeRunning = MCPForUnityBridge.IsRunning; GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 14 }; EditorGUILayout.LabelField("Unity Bridge", sectionTitleStyle); EditorGUILayout.Space(8); EditorGUILayout.BeginHorizontal(); Color bridgeColor = isUnityBridgeRunning ? Color.green : Color.red; Rect bridgeStatusRect = GUILayoutUtility.GetRect(0, 28, GUILayout.Width(24)); DrawStatusDot(bridgeStatusRect, bridgeColor, 16); GUIStyle bridgeStatusStyle = new GUIStyle(EditorStyles.label) { fontSize = 12, fontStyle = FontStyle.Bold }; EditorGUILayout.LabelField(isUnityBridgeRunning ? "Running" : "Stopped", bridgeStatusStyle, GUILayout.Height(28)); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(8); if (GUILayout.Button(isUnityBridgeRunning ? "Stop Bridge" : "Start Bridge", GUILayout.Height(32))) { ToggleUnityBridge(); } EditorGUILayout.Space(5); EditorGUILayout.EndVertical(); } private void DrawValidationSection() { EditorGUILayout.BeginVertical(EditorStyles.helpBox); GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 14 }; EditorGUILayout.LabelField("Script Validation", sectionTitleStyle); EditorGUILayout.Space(8); EditorGUI.BeginChangeCheck(); validationLevelIndex = EditorGUILayout.Popup("Validation Level", validationLevelIndex, validationLevelOptions, GUILayout.Height(20)); if (EditorGUI.EndChangeCheck()) { SaveValidationLevelSetting(); } EditorGUILayout.Space(8); string description = GetValidationLevelDescription(validationLevelIndex); EditorGUILayout.HelpBox(description, MessageType.Info); EditorGUILayout.Space(4); // (Show Debug Logs toggle moved to header) EditorGUILayout.Space(2); EditorGUILayout.EndVertical(); } private void DrawUnifiedClientConfiguration() { EditorGUILayout.BeginVertical(EditorStyles.helpBox); GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 14 }; EditorGUILayout.LabelField("MCP Client Configuration", sectionTitleStyle); EditorGUILayout.Space(10); // (Auto-connect toggle removed per design) // Client selector string[] clientNames = mcpClients.clients.Select(c => c.name).ToArray(); EditorGUI.BeginChangeCheck(); selectedClientIndex = EditorGUILayout.Popup("Select Client", selectedClientIndex, clientNames, GUILayout.Height(20)); if (EditorGUI.EndChangeCheck()) { selectedClientIndex = Mathf.Clamp(selectedClientIndex, 0, mcpClients.clients.Count - 1); } EditorGUILayout.Space(10); if (mcpClients.clients.Count > 0 && selectedClientIndex < mcpClients.clients.Count) { McpClient selectedClient = mcpClients.clients[selectedClientIndex]; DrawClientConfigurationCompact(selectedClient); } EditorGUILayout.Space(5); EditorGUILayout.EndVertical(); } private void AutoFirstRunSetup() { try { // Project-scoped one-time flag string projectPath = Application.dataPath ?? string.Empty; string key = $"MCPForUnity.AutoRegistered.{ComputeSha1(projectPath)}"; if (EditorPrefs.GetBool(key, false)) { return; } // Attempt client registration using discovered Python server dir pythonDirOverride ??= EditorPrefs.GetString("MCPForUnity.PythonDirOverride", null); string pythonDir = !string.IsNullOrEmpty(pythonDirOverride) ? pythonDirOverride : FindPackagePythonDirectory(); if (!string.IsNullOrEmpty(pythonDir) && File.Exists(Path.Combine(pythonDir, "server.py"))) { bool anyRegistered = false; foreach (McpClient client in mcpClients.clients) { try { if (client.mcpType == McpTypes.ClaudeCode) { // Only attempt if Claude CLI is present if (!IsClaudeConfigured() && !string.IsNullOrEmpty(ExecPath.ResolveClaude())) { RegisterWithClaudeCode(pythonDir); anyRegistered = true; } } else { CheckMcpConfiguration(client); bool alreadyConfigured = client.status == McpStatus.Configured; if (!alreadyConfigured) { ConfigureMcpClient(client); anyRegistered = true; } } } catch (Exception ex) { MCPForUnity.Editor.Helpers.McpLog.Warn($"Auto-setup client '{client.name}' failed: {ex.Message}"); } } lastClientRegisteredOk = anyRegistered || IsCursorConfigured(pythonDir) || CodexConfigHelper.IsCodexConfigured(pythonDir) || IsClaudeConfigured(); } // Ensure the bridge is listening and has a fresh saved port if (!MCPForUnityBridge.IsRunning) { try { MCPForUnityBridge.StartAutoConnect(); isUnityBridgeRunning = MCPForUnityBridge.IsRunning; Repaint(); } catch (Exception ex) { MCPForUnity.Editor.Helpers.McpLog.Warn($"Auto-setup StartAutoConnect failed: {ex.Message}"); } } // Verify bridge with a quick ping lastBridgeVerifiedOk = VerifyBridgePing(MCPForUnityBridge.GetCurrentPort()); EditorPrefs.SetBool(key, true); } catch (Exception e) { MCPForUnity.Editor.Helpers.McpLog.Warn($"MCP for Unity auto-setup skipped: {e.Message}"); } } private static string ComputeSha1(string input) { try { using SHA1 sha1 = SHA1.Create(); byte[] bytes = Encoding.UTF8.GetBytes(input ?? string.Empty); byte[] hash = sha1.ComputeHash(bytes); StringBuilder sb = new StringBuilder(hash.Length * 2); foreach (byte b in hash) { sb.Append(b.ToString("x2")); } return sb.ToString(); } catch { return ""; } } private void RunSetupNow() { // Force a one-shot setup regardless of first-run flag try { pythonDirOverride ??= EditorPrefs.GetString("MCPForUnity.PythonDirOverride", null); string pythonDir = !string.IsNullOrEmpty(pythonDirOverride) ? pythonDirOverride : FindPackagePythonDirectory(); if (string.IsNullOrEmpty(pythonDir) || !File.Exists(Path.Combine(pythonDir, "server.py"))) { EditorUtility.DisplayDialog("Setup", "Python server not found. Please select UnityMcpServer/src.", "OK"); return; } bool anyRegistered = false; foreach (McpClient client in mcpClients.clients) { try { if (client.mcpType == McpTypes.ClaudeCode) { if (!IsClaudeConfigured()) { RegisterWithClaudeCode(pythonDir); anyRegistered = true; } } else { CheckMcpConfiguration(client); bool alreadyConfigured = client.status == McpStatus.Configured; if (!alreadyConfigured) { ConfigureMcpClient(client); anyRegistered = true; } } } catch (Exception ex) { UnityEngine.Debug.LogWarning($"Setup client '{client.name}' failed: {ex.Message}"); } } lastClientRegisteredOk = anyRegistered || IsCursorConfigured(pythonDir) || CodexConfigHelper.IsCodexConfigured(pythonDir) || IsClaudeConfigured(); // Restart/ensure bridge MCPForUnityBridge.StartAutoConnect(); isUnityBridgeRunning = MCPForUnityBridge.IsRunning; // Verify lastBridgeVerifiedOk = VerifyBridgePing(MCPForUnityBridge.GetCurrentPort()); Repaint(); } catch (Exception e) { EditorUtility.DisplayDialog("Setup Failed", e.Message, "OK"); } } private static bool IsCursorConfigured(string pythonDir) { try { string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cursor", "mcp.json") : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cursor", "mcp.json"); if (!File.Exists(configPath)) return false; string json = File.ReadAllText(configPath); dynamic cfg = JsonConvert.DeserializeObject(json); var servers = cfg?.mcpServers; if (servers == null) return false; var unity = servers.unityMCP ?? servers.UnityMCP; if (unity == null) return false; var args = unity.args; if (args == null) return false; // Prefer exact extraction of the --directory value and compare normalized paths string[] strArgs = ((System.Collections.Generic.IEnumerable)args) .Select(x => x?.ToString() ?? string.Empty) .ToArray(); string dir = McpConfigFileHelper.ExtractDirectoryArg(strArgs); if (string.IsNullOrEmpty(dir)) return false; return McpConfigFileHelper.PathsEqual(dir, pythonDir); } catch { return false; } } private static bool IsClaudeConfigured() { try { string claudePath = ExecPath.ResolveClaude(); if (string.IsNullOrEmpty(claudePath)) return false; // Only prepend PATH on Unix string pathPrepend = null; if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { pathPrepend = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin" : "/usr/local/bin:/usr/bin:/bin"; } if (!ExecPath.TryRun(claudePath, "mcp list", workingDir: null, out var stdout, out var stderr, 5000, pathPrepend)) { return false; } return (stdout ?? string.Empty).IndexOf("UnityMCP", StringComparison.OrdinalIgnoreCase) >= 0; } catch { return false; } } private static bool VerifyBridgePing(int port) { // Use strict framed protocol to match bridge (FRAMING=1) const int ConnectTimeoutMs = 1000; const int FrameTimeoutMs = 30000; // match bridge frame I/O timeout try { using TcpClient client = new TcpClient(); var connectTask = client.ConnectAsync(IPAddress.Loopback, port); if (!connectTask.Wait(ConnectTimeoutMs)) return false; using NetworkStream 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) { UnityEngine.Debug.LogWarning("MCP for Unity: Bridge handshake missing FRAMING=1"); return false; } // 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); bool ok = !string.IsNullOrEmpty(response) && response.IndexOf("pong", StringComparison.OrdinalIgnoreCase) >= 0; if (!ok) { UnityEngine.Debug.LogWarning($"MCP for Unity: Framed ping failed; response='{response}'"); } return ok; } catch (Exception ex) { UnityEngine.Debug.LogWarning($"MCP for Unity: VerifyBridgePing error: {ex.Message}"); return false; } } // 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()); } private void DrawClientConfigurationCompact(McpClient mcpClient) { // Special pre-check for Claude Code: if CLI missing, reflect in status UI if (mcpClient.mcpType == McpTypes.ClaudeCode) { string claudeCheck = ExecPath.ResolveClaude(); if (string.IsNullOrEmpty(claudeCheck)) { mcpClient.configStatus = "Claude Not Found"; mcpClient.status = McpStatus.NotConfigured; } } // Pre-check for clients that require uv (all except Claude Code) bool uvRequired = mcpClient.mcpType != McpTypes.ClaudeCode; bool uvMissingEarly = false; if (uvRequired) { string uvPathEarly = FindUvPath(); if (string.IsNullOrEmpty(uvPathEarly)) { uvMissingEarly = true; mcpClient.configStatus = "uv Not Found"; mcpClient.status = McpStatus.NotConfigured; } } // Status display EditorGUILayout.BeginHorizontal(); Rect statusRect = GUILayoutUtility.GetRect(0, 28, GUILayout.Width(24)); Color statusColor = GetStatusColor(mcpClient.status); DrawStatusDot(statusRect, statusColor, 16); GUIStyle clientStatusStyle = new GUIStyle(EditorStyles.label) { fontSize = 12, fontStyle = FontStyle.Bold }; EditorGUILayout.LabelField(mcpClient.configStatus, clientStatusStyle, GUILayout.Height(28)); EditorGUILayout.EndHorizontal(); // When Claude CLI is missing, show a clear install hint directly below status if (mcpClient.mcpType == McpTypes.ClaudeCode && string.IsNullOrEmpty(ExecPath.ResolveClaude())) { GUIStyle installHintStyle = new GUIStyle(clientStatusStyle); installHintStyle.normal.textColor = new Color(1f, 0.5f, 0f); // orange EditorGUILayout.BeginHorizontal(); GUIContent installText = new GUIContent("Make sure Claude Code is installed!"); Vector2 textSize = installHintStyle.CalcSize(installText); EditorGUILayout.LabelField(installText, installHintStyle, GUILayout.Height(22), GUILayout.Width(textSize.x + 2), GUILayout.ExpandWidth(false)); GUIStyle helpLinkStyle = new GUIStyle(EditorStyles.linkLabel) { fontStyle = FontStyle.Bold }; GUILayout.Space(6); if (GUILayout.Button("[HELP]", helpLinkStyle, GUILayout.Height(22), GUILayout.ExpandWidth(false))) { Application.OpenURL("https://github.com/CoplayDev/unity-mcp/wiki/Troubleshooting-Unity-MCP-and-Claude-Code"); } EditorGUILayout.EndHorizontal(); } EditorGUILayout.Space(10); // If uv is missing for required clients, show hint and picker then exit early to avoid showing other controls if (uvRequired && uvMissingEarly) { GUIStyle installHintStyle2 = new GUIStyle(EditorStyles.label) { fontSize = 12, fontStyle = FontStyle.Bold, wordWrap = false }; installHintStyle2.normal.textColor = new Color(1f, 0.5f, 0f); EditorGUILayout.BeginHorizontal(); GUIContent installText2 = new GUIContent("Make sure uv is installed!"); Vector2 sz = installHintStyle2.CalcSize(installText2); EditorGUILayout.LabelField(installText2, installHintStyle2, GUILayout.Height(22), GUILayout.Width(sz.x + 2), GUILayout.ExpandWidth(false)); GUIStyle helpLinkStyle2 = new GUIStyle(EditorStyles.linkLabel) { fontStyle = FontStyle.Bold }; GUILayout.Space(6); if (GUILayout.Button("[HELP]", helpLinkStyle2, GUILayout.Height(22), GUILayout.ExpandWidth(false))) { Application.OpenURL("https://github.com/CoplayDev/unity-mcp/wiki/Troubleshooting-Unity-MCP-and-Cursor,-VSCode-&-Windsurf"); } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(8); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Choose uv Install Location", GUILayout.Width(260), GUILayout.Height(22))) { string suggested = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/opt/homebrew/bin" : Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); string picked = EditorUtility.OpenFilePanel("Select 'uv' binary", suggested, ""); if (!string.IsNullOrEmpty(picked)) { EditorPrefs.SetString("MCPForUnity.UvPath", picked); ConfigureMcpClient(mcpClient); Repaint(); } } EditorGUILayout.EndHorizontal(); return; } // Action buttons in horizontal layout EditorGUILayout.BeginHorizontal(); if (mcpClient.mcpType == McpTypes.VSCode) { if (GUILayout.Button("Auto Configure", GUILayout.Height(32))) { ConfigureMcpClient(mcpClient); } } else if (mcpClient.mcpType == McpTypes.ClaudeCode) { bool claudeAvailable = !string.IsNullOrEmpty(ExecPath.ResolveClaude()); if (claudeAvailable) { bool isConfigured = mcpClient.status == McpStatus.Configured; string buttonText = isConfigured ? "Unregister MCP for Unity with Claude Code" : "Register with Claude Code"; if (GUILayout.Button(buttonText, GUILayout.Height(32))) { if (isConfigured) { UnregisterWithClaudeCode(); } else { string pythonDir = FindPackagePythonDirectory(); RegisterWithClaudeCode(pythonDir); } } // Hide the picker once a valid binary is available EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); GUIStyle pathLabelStyle = new GUIStyle(EditorStyles.miniLabel) { wordWrap = true }; string resolvedClaude = ExecPath.ResolveClaude(); EditorGUILayout.LabelField($"Claude CLI: {resolvedClaude}", pathLabelStyle); EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); } // CLI picker row (only when not found) EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); if (!claudeAvailable) { // Only show the picker button in not-found state (no redundant "not found" label) if (GUILayout.Button("Choose Claude Install Location", GUILayout.Width(260), GUILayout.Height(22))) { string suggested = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/opt/homebrew/bin" : Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); string picked = EditorUtility.OpenFilePanel("Select 'claude' CLI", suggested, ""); if (!string.IsNullOrEmpty(picked)) { ExecPath.SetClaudeCliPath(picked); // Auto-register after setting a valid path string pythonDir = FindPackagePythonDirectory(); RegisterWithClaudeCode(pythonDir); Repaint(); } } } EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); } else { if (GUILayout.Button($"Auto Configure", GUILayout.Height(32))) { ConfigureMcpClient(mcpClient); } } if (mcpClient.mcpType != McpTypes.ClaudeCode) { if (GUILayout.Button("Manual Setup", GUILayout.Height(32))) { string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? mcpClient.windowsConfigPath : mcpClient.linuxConfigPath; if (mcpClient.mcpType == McpTypes.VSCode) { string pythonDir = FindPackagePythonDirectory(); string uvPath = FindUvPath(); if (uvPath == null) { UnityEngine.Debug.LogError("UV package manager not found. Cannot configure VSCode."); return; } // VSCode now reads from mcp.json with a top-level "servers" block var vscodeConfig = new { servers = new { unityMCP = new { command = uvPath, args = new[] { "run", "--directory", pythonDir, "server.py" } } } }; JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented }; string manualConfigJson = JsonConvert.SerializeObject(vscodeConfig, jsonSettings); VSCodeManualSetupWindow.ShowWindow(configPath, manualConfigJson); } else { ShowManualInstructionsWindow(configPath, mcpClient); } } } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(8); // Quick info (hide when Claude is not found to avoid confusion) bool hideConfigInfo = (mcpClient.mcpType == McpTypes.ClaudeCode && string.IsNullOrEmpty(ExecPath.ResolveClaude())) || ((mcpClient.mcpType != McpTypes.ClaudeCode) && string.IsNullOrEmpty(FindUvPath())); if (!hideConfigInfo) { GUIStyle configInfoStyle = new GUIStyle(EditorStyles.miniLabel) { fontSize = 10 }; EditorGUILayout.LabelField($"Config: {Path.GetFileName(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? mcpClient.windowsConfigPath : mcpClient.linuxConfigPath)}", configInfoStyle); } } private void ToggleUnityBridge() { if (isUnityBridgeRunning) { MCPForUnityBridge.Stop(); } else { MCPForUnityBridge.Start(); } // Reflect the actual state post-operation (avoid optimistic toggle) isUnityBridgeRunning = MCPForUnityBridge.IsRunning; Repaint(); } // New method to show manual instructions without changing status private void ShowManualInstructionsWindow(string configPath, McpClient mcpClient) { // Get the Python directory path using Package Manager API string pythonDir = FindPackagePythonDirectory(); // Build manual JSON centrally using the shared builder string uvPathForManual = FindUvPath(); if (uvPathForManual == null) { UnityEngine.Debug.LogError("UV package manager not found. Cannot generate manual configuration."); return; } string manualConfig = mcpClient?.mcpType == McpTypes.Codex ? CodexConfigHelper.BuildCodexServerBlock(uvPathForManual, McpConfigFileHelper.ResolveServerDirectory(pythonDir, null)).TrimEnd() + Environment.NewLine : ConfigJsonBuilder.BuildManualConfigJson(uvPathForManual, pythonDir, mcpClient); ManualConfigEditorWindow.ShowWindow(configPath, manualConfig, mcpClient); } private string FindPackagePythonDirectory() { // Use shared helper for consistent path resolution across both windows return McpPathResolver.FindPackagePythonDirectory(debugLogsEnabled); } private string ConfigureMcpClient(McpClient mcpClient) { try { // Use shared helper for consistent config path resolution string configPath = McpConfigurationHelper.GetClientConfigPath(mcpClient); // Create directory if it doesn't exist McpConfigurationHelper.EnsureConfigDirectoryExists(configPath); // Find the server.py file location using shared helper string pythonDir = FindPackagePythonDirectory(); if (pythonDir == null || !File.Exists(Path.Combine(pythonDir, "server.py"))) { ShowManualInstructionsWindow(configPath, mcpClient); return "Manual Configuration Required"; } string result = mcpClient.mcpType == McpTypes.Codex ? McpConfigurationHelper.ConfigureCodexClient(pythonDir, configPath, mcpClient) : McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, mcpClient); // Update the client status after successful configuration if (result == "Configured successfully") { mcpClient.SetStatus(McpStatus.Configured); } return result; } catch (Exception e) { // Determine the config file path based on OS for error message string configPath = ""; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { configPath = mcpClient.windowsConfigPath; } else if ( RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ) { configPath = string.IsNullOrEmpty(mcpClient.macConfigPath) ? mcpClient.linuxConfigPath : mcpClient.macConfigPath; } else if ( RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ) { configPath = mcpClient.linuxConfigPath; } ShowManualInstructionsWindow(configPath, mcpClient); UnityEngine.Debug.LogError( $"Failed to configure {mcpClient.name}: {e.Message}\n{e.StackTrace}" ); return $"Failed to configure {mcpClient.name}"; } } private void LoadValidationLevelSetting() { string savedLevel = EditorPrefs.GetString("MCPForUnity_ScriptValidationLevel", "standard"); validationLevelIndex = savedLevel.ToLower() switch { "basic" => 0, "standard" => 1, "comprehensive" => 2, "strict" => 3, _ => 1 // Default to Standard }; } private void SaveValidationLevelSetting() { string levelString = validationLevelIndex switch { 0 => "basic", 1 => "standard", 2 => "comprehensive", 3 => "strict", _ => "standard" }; EditorPrefs.SetString("MCPForUnity_ScriptValidationLevel", levelString); } private string GetValidationLevelDescription(int index) { return index switch { 0 => "Only basic syntax checks (braces, quotes, comments)", 1 => "Syntax checks + Unity best practices and warnings", 2 => "All checks + semantic analysis and performance warnings", 3 => "Full semantic validation with namespace/type resolution (requires Roslyn)", _ => "Standard validation" }; } private void CheckMcpConfiguration(McpClient mcpClient) { try { // Special handling for Claude Code if (mcpClient.mcpType == McpTypes.ClaudeCode) { CheckClaudeCodeConfiguration(mcpClient); return; } // Use shared helper for consistent config path resolution string configPath = McpConfigurationHelper.GetClientConfigPath(mcpClient); if (!File.Exists(configPath)) { mcpClient.SetStatus(McpStatus.NotConfigured); return; } string configJson = File.ReadAllText(configPath); // Use the same path resolution as configuration to avoid false "Incorrect Path" in dev mode string pythonDir = FindPackagePythonDirectory(); // Use switch statement to handle different client types, extracting common logic string[] args = null; bool configExists = false; switch (mcpClient.mcpType) { case McpTypes.VSCode: dynamic config = JsonConvert.DeserializeObject(configJson); // New schema: top-level servers if (config?.servers?.unityMCP != null) { args = config.servers.unityMCP.args.ToObject(); configExists = true; } // Back-compat: legacy mcp.servers else if (config?.mcp?.servers?.unityMCP != null) { args = config.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: // Standard MCP configuration check for Claude Desktop, Cursor, etc. McpConfig standardConfig = JsonConvert.DeserializeObject(configJson); if (standardConfig?.mcpServers?.unityMCP != null) { args = standardConfig.mcpServers.unityMCP.args; configExists = true; } break; } // Common logic for checking configuration status if (configExists) { string configuredDir = McpConfigFileHelper.ExtractDirectoryArg(args); bool matches = !string.IsNullOrEmpty(configuredDir) && McpConfigFileHelper.PathsEqual(configuredDir, pythonDir); if (matches) { mcpClient.SetStatus(McpStatus.Configured); } else { // Attempt auto-rewrite once if the package path changed try { string rewriteResult = mcpClient.mcpType == McpTypes.Codex ? McpConfigurationHelper.ConfigureCodexClient(pythonDir, configPath, mcpClient) : McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, mcpClient); if (rewriteResult == "Configured successfully") { if (debugLogsEnabled) { MCPForUnity.Editor.Helpers.McpLog.Info($"Auto-updated MCP config for '{mcpClient.name}' to new path: {pythonDir}", always: false); } mcpClient.SetStatus(McpStatus.Configured); } else { mcpClient.SetStatus(McpStatus.IncorrectPath); } } catch (Exception ex) { mcpClient.SetStatus(McpStatus.IncorrectPath); if (debugLogsEnabled) { UnityEngine.Debug.LogWarning($"MCP for Unity: Auto-config rewrite failed for '{mcpClient.name}': {ex.Message}"); } } } } else { mcpClient.SetStatus(McpStatus.MissingConfig); } } catch (Exception e) { mcpClient.SetStatus(McpStatus.Error, e.Message); } } private void RegisterWithClaudeCode(string pythonDir) { // Resolve claude and uv; then run register command string claudePath = ExecPath.ResolveClaude(); if (string.IsNullOrEmpty(claudePath)) { UnityEngine.Debug.LogError("MCP for Unity: Claude CLI not found. Set a path in this window or install the CLI, then try again."); return; } string uvPath = ExecPath.ResolveUv() ?? "uv"; // Prefer embedded/dev path when available string srcDir = !string.IsNullOrEmpty(pythonDirOverride) ? pythonDirOverride : FindPackagePythonDirectory(); if (string.IsNullOrEmpty(srcDir)) srcDir = pythonDir; string args = $"mcp add UnityMCP -- \"{uvPath}\" run --directory \"{srcDir}\" server.py"; string projectDir = Path.GetDirectoryName(Application.dataPath); // Ensure PATH includes common locations on Unix; on Windows leave PATH as-is string pathPrepend = null; if (Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.LinuxEditor) { pathPrepend = Application.platform == RuntimePlatform.OSXEditor ? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin" : "/usr/local/bin:/usr/bin:/bin"; } 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) { // Treat as success if Claude reports existing registration var existingClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode); if (existingClient != null) CheckClaudeCodeConfiguration(existingClient); Repaint(); UnityEngine.Debug.Log("MCP-FOR-UNITY: MCP for Unity already registered with Claude Code."); } else { UnityEngine.Debug.LogError($"MCP for Unity: Failed to start Claude CLI.\n{stderr}\n{stdout}"); } return; } // Update status var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode); if (claudeClient != null) CheckClaudeCodeConfiguration(claudeClient); Repaint(); UnityEngine.Debug.Log("MCP-FOR-UNITY: Registered with Claude Code."); } private void UnregisterWithClaudeCode() { string claudePath = ExecPath.ResolveClaude(); if (string.IsNullOrEmpty(claudePath)) { UnityEngine.Debug.LogError("MCP for Unity: Claude CLI not found. Set a path in this window or install the CLI, then try again."); return; } string projectDir = Path.GetDirectoryName(Application.dataPath); string pathPrepend = Application.platform == RuntimePlatform.OSXEditor ? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin" : null; // On Windows, don't modify PATH - use system PATH as-is // Determine if Claude has a "UnityMCP" server registered by using exit codes from `claude mcp get ` string[] candidateNamesForGet = { "UnityMCP", "unityMCP", "unity-mcp", "UnityMcpServer" }; List existingNames = new List(); foreach (var candidate in candidateNamesForGet) { if (ExecPath.TryRun(claudePath, $"mcp get {candidate}", projectDir, out var getStdout, out var getStderr, 7000, pathPrepend)) { // Success exit code indicates the server exists existingNames.Add(candidate); } } if (existingNames.Count == 0) { // Nothing to unregister – set status and bail early var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode); if (claudeClient != null) { claudeClient.SetStatus(McpStatus.NotConfigured); UnityEngine.Debug.Log("Claude CLI reports no MCP for Unity server via 'mcp get' - setting status to NotConfigured and aborting unregister."); Repaint(); } return; } // Try different possible server names string[] possibleNames = { "UnityMCP", "unityMCP", "unity-mcp", "UnityMcpServer" }; bool success = false; foreach (string serverName in possibleNames) { if (ExecPath.TryRun(claudePath, $"mcp remove {serverName}", projectDir, out var stdout, out var stderr, 10000, pathPrepend)) { success = true; UnityEngine.Debug.Log($"MCP for Unity: Successfully removed MCP server: {serverName}"); break; } else if (!string.IsNullOrEmpty(stderr) && !stderr.Contains("No MCP server found", StringComparison.OrdinalIgnoreCase)) { // If it's not a "not found" error, log it and stop trying UnityEngine.Debug.LogWarning($"Error removing {serverName}: {stderr}"); break; } } if (success) { var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode); if (claudeClient != null) { // Optimistically flip to NotConfigured; then verify claudeClient.SetStatus(McpStatus.NotConfigured); CheckClaudeCodeConfiguration(claudeClient); } Repaint(); UnityEngine.Debug.Log("MCP for Unity: MCP server successfully unregistered from Claude Code."); } else { // If no servers were found to remove, they're already unregistered // Force status to NotConfigured and update the UI UnityEngine.Debug.Log("No MCP servers found to unregister - already unregistered."); var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode); if (claudeClient != null) { claudeClient.SetStatus(McpStatus.NotConfigured); CheckClaudeCodeConfiguration(claudeClient); } Repaint(); } } // Removed unused ParseTextOutput private string FindUvPath() { try { return MCPForUnity.Editor.Helpers.ServerInstaller.FindUvPath(); } catch { return null; } } // Validation and platform-specific scanning are handled by ServerInstaller.FindUvPath() // Windows-specific discovery removed; use ServerInstaller.FindUvPath() instead // Removed unused FindClaudeCommand private void CheckClaudeCodeConfiguration(McpClient mcpClient) { try { // Get the Unity project directory to check project-specific config string unityProjectDir = Application.dataPath; string projectDir = Path.GetDirectoryName(unityProjectDir); // Read the global Claude config file (honor macConfigPath on macOS) string configPath; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) configPath = mcpClient.windowsConfigPath; else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) configPath = string.IsNullOrEmpty(mcpClient.macConfigPath) ? mcpClient.linuxConfigPath : mcpClient.macConfigPath; else configPath = mcpClient.linuxConfigPath; if (debugLogsEnabled) { MCPForUnity.Editor.Helpers.McpLog.Info($"Checking Claude config at: {configPath}", always: false); } if (!File.Exists(configPath)) { UnityEngine.Debug.LogWarning($"Claude config file not found at: {configPath}"); mcpClient.SetStatus(McpStatus.NotConfigured); return; } string configJson = File.ReadAllText(configPath); dynamic claudeConfig = JsonConvert.DeserializeObject(configJson); // Check for "UnityMCP" server in the mcpServers section (current format) if (claudeConfig?.mcpServers != null) { var servers = claudeConfig.mcpServers; if (servers.UnityMCP != null || servers.unityMCP != null) { // Found MCP for Unity configured mcpClient.SetStatus(McpStatus.Configured); return; } } // Also check if there's a project-specific configuration for this Unity project (legacy format) if (claudeConfig?.projects != null) { // Look for the project path in the config foreach (var project in claudeConfig.projects) { string projectPath = project.Name; // Normalize paths for comparison (handle forward/back slash differences) string normalizedProjectPath = Path.GetFullPath(projectPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); string normalizedProjectDir = Path.GetFullPath(projectDir).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); if (string.Equals(normalizedProjectPath, normalizedProjectDir, StringComparison.OrdinalIgnoreCase) && project.Value?.mcpServers != null) { // Check for "UnityMCP" (case variations) var servers = project.Value.mcpServers; if (servers.UnityMCP != null || servers.unityMCP != null) { // Found MCP for Unity configured for this project mcpClient.SetStatus(McpStatus.Configured); return; } } } } // No configuration found for this project mcpClient.SetStatus(McpStatus.NotConfigured); } catch (Exception e) { UnityEngine.Debug.LogWarning($"Error checking Claude Code config: {e.Message}"); mcpClient.SetStatus(McpStatus.Error, e.Message); } } private 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 (existing code) 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; } } }