using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; using System.Threading.Tasks; using UnityEditor; using UnityEditor.PackageManager; using UnityEditor.PackageManager.Requests; using UnityEngine; using Newtonsoft.Json; using SynapticAIPro; namespace SynapticPro { /// /// MCP Server Setup and Local CLI Management Window /// One-touch setup for MCP server and configuration of various AI tools /// public class NexusMCPSetupWindow : EditorWindow { [MenuItem("Tools/Synaptic Pro/Synaptic Setup", false, 0)] public static void ShowWindow() { var window = GetWindow("Synaptic Pro Setup"); // 小さい画面でも使えるようminSizeを緩和(旧: 800x800で固定気味) window.minSize = new Vector2(480, 480); window.Show(); } private NexusMCPSetupManager mcpSetupManager; private NexusMCPSetupManager.SetupStatus mcpStatus; private Vector2 scrollPosition; private bool mcpServerRunning = false; // Tabs private int selectedTab = 0; private string[] tabNames = new string[] { "AI Connection", "HTTP Server", "Help" }; // MCP Settings private int mcpPort = 8090; private int wsPort = 8090; // HTTP Server Settings private int httpPort = 8086; private bool httpServerRunning = false; // Use project-specific keys for HTTP settings private static string ProjectKey => Application.dataPath.GetHashCode().ToString("X8"); private static string PREF_HTTP_PORT => $"SynapticPro_HTTP_Port_{ProjectKey}"; private static string PREF_HTTP_AUTO_START => $"SynapticPro_HTTP_AutoStart_{ProjectKey}"; // External HTTP Server Process private static System.Diagnostics.Process externalHttpProcess = null; private static bool externalHttpRunning = false; private static bool externalAutoStartAttempted = false; // Port-probe throttle. Probing runs on a background thread; UI reads the cached // `externalHttpRunning` so OnGUI never blocks on a TCP connect. private static double lastPortCheckTime = -100; private static bool portCheckInFlight = false; private const double PORT_CHECK_INTERVAL_SEC = 2.0; private const int PORT_CHECK_TIMEOUT_MS = 500; // Auto-Update private static bool updateCheckDone = false; private static bool updateAvailable = false; private static string latestVersion = ""; private static string updateUrl = ""; private static string updateMethod = "browser"; // "browser" or "auto" private static bool isDownloadingUpdate = false; // Distribution type: "assetstore" or "booth" // Asset Store版はfalse(ブラウザでAsset Storeに飛ばす)、BOOTH/サイト版はtrue(自動ダウンロード) private static readonly bool ENABLE_SELF_UPDATE = false; // Asset Store版リリース時にfalseに変更 public static bool HttpAutoStartEnabled { get => EditorPrefs.GetBool(PREF_HTTP_AUTO_START, false); set => EditorPrefs.SetBool(PREF_HTTP_AUTO_START, value); } // Animation private bool isConnecting = false; private float animationTime = 0f; private const float CONNECTION_TIMEOUT = 60f; // 60 second timeout // Setup state management private bool mcpConfigured = false; // AI Client selection (v1.1.3) private enum AIClientType { ClaudeDesktopOrCursor, // Full mode (index.js) - 246 tools CursorOrLMStudioEssential, // Essential mode (index-essential.js) - 80 tools GitHubCopilot, // Dynamic mode (hub-server.js) - 8→dynamic tools TokenSuperSaveMode // Experimental: 3 meta-tools only (index-supersave.js) } private AIClientType selectedAIClient = AIClientType.TokenSuperSaveMode; private string[] connectingMessages = new string[] { "Preparing AI Connection", "Starting MCP Server", "Establishing connection with desktop AI apps", "Auto-generating configuration files", "AI connection almost ready" }; private GUIStyle headerStyle; private GUIStyle setupButtonStyle; private GUIStyle statusStyle; private void OnEnable() { mcpSetupManager = NexusMCPSetupManager.Instance; // Load HTTP Server settings first (no blocking) httpPort = EditorPrefs.GetInt(PREF_HTTP_PORT, 8086); // Check if server is still running CheckExternalHttpServerStatus(); // Try auto-start TryHttpAutoStart(); // Check for updates (background, 1日1回) CheckForUpdates(); // Refresh MCP status in background thread (completely non-blocking) System.Threading.ThreadPool.QueueUserWorkItem(async _ => { try { mcpStatus = await mcpSetupManager.CheckSetupStatus(); // UI更新はメインスレッドで EditorApplication.delayCall += () => { if (this) Repaint(); }; } catch { } }); } private void OnDisable() { // Save HTTP Server settings EditorPrefs.SetInt(PREF_HTTP_PORT, httpPort); // サーバーはここで止めない(ドメインリロードで毎回止まるため) // Unity終了時はRegisterQuitHandlerで停止する } // Unity終了時にNode.jsプロセスを確実に停止 [UnityEditor.InitializeOnLoadMethod] private static void RegisterQuitHandler() { EditorApplication.quitting += () => { var port = EditorPrefs.GetInt(PREF_HTTP_PORT, 8086); try { // ポートを使っているプロセスを強制終了 if (Application.platform == RuntimePlatform.WindowsEditor) { var p = new System.Diagnostics.Process { StartInfo = new System.Diagnostics.ProcessStartInfo { FileName = "cmd.exe", Arguments = $"/c \"for /f \"tokens=5\" %a in ('netstat -aon ^| findstr :{port} ^| findstr LISTENING') do taskkill /PID %a /F\"", UseShellExecute = false, CreateNoWindow = true } }; p.Start(); p.WaitForExit(3000); } else { var p = new System.Diagnostics.Process { StartInfo = new System.Diagnostics.ProcessStartInfo { FileName = "/bin/bash", Arguments = $"-c \"lsof -ti:{port} | xargs kill -9 2>/dev/null\"", UseShellExecute = false, CreateNoWindow = true } }; p.Start(); p.WaitForExit(3000); } SynLog.Info($"[Synaptic] Killed HTTP server process on port {port} (Unity quitting)"); } catch { } }; } // Unity起動時にアップデートチェック(Setup Windowを開かなくても通知) /// /// After domain reload, recover the previously spawned detached /// node.exe by re-attaching to its PID stored in SessionState. /// Only the WebSocket needs reconnecting — the HTTP process itself /// was started detached and survives across reloads. /// [InitializeOnLoadMethod] private static void RestoreDetachedHttpServerOnReload() { EditorApplication.delayCall += () => { // Windows detached path: recover by PID stored in SessionState. if (Application.platform == RuntimePlatform.WindowsEditor && SynapticDetachedProcess.IsStoredProcessAlive(out int pid, out int wPort)) { externalHttpRunning = true; EditorPrefs.SetInt(PREF_HTTP_PORT, wPort); SynLog.Info($"[Synaptic] Recovered detached HTTP server PID={pid} on port {wPort}. Reconnecting WebSocket."); _ = ReconnectWebSocketOnlyAsync(wPort); return; } // Generic path (Mac/Linux, or Windows fallback): probe last-used // port; if a server is listening, just reconnect WS. int port = EditorPrefs.GetInt(PREF_HTTP_PORT, 8086); if (IsPortListeningStatic(port)) { externalHttpRunning = true; SynLog.Info($"[Synaptic] HTTP server alive on port {port} after reload. Reconnecting WebSocket."); _ = ReconnectWebSocketOnlyAsync(port); return; } // Server is dead. If the user opted into HTTP auto-start, // bring the Setup window up so its OnEnable → TryHttpAutoStart // path can re-spawn the node process. Without this nudge the // NexusHTTPWebSocketClient retry loop just hammers a closed // port forever (the client cannot start its own server). if (Application.platform == RuntimePlatform.WindowsEditor) { // Stored PID is stale and port dead — clear for fresh spawn. SynapticDetachedProcess.ClearStoredPid(); } if (HttpAutoStartEnabled) { SynLog.Info("[Synaptic] HTTP server not running after reload; opening Setup window to auto-restart."); // GetWindow opens existing or creates new; OnEnable fires // TryHttpAutoStart which spawns the node process. EditorWindow.GetWindow("Synaptic Setup", true); } }; } /// /// Static port-alive probe usable from [InitializeOnLoadMethod]. /// Lightweight TCP connect with short timeout. /// private static bool IsPortListeningStatic(int port) { try { using (var tcp = new System.Net.Sockets.TcpClient()) { var ar = tcp.BeginConnect("127.0.0.1", port, null, null); bool ok = ar.AsyncWaitHandle.WaitOne(500); if (!ok) return false; try { tcp.EndConnect(ar); return true; } catch { return false; } } } catch { return false; } } /// /// Reconnect WebSocket to an already-running HTTP server after domain /// reload. Does not start the server or open any UI. /// private static async System.Threading.Tasks.Task ReconnectWebSocketOnlyAsync(int port) { // Light delay so the editor finishes loading other systems first. await System.Threading.Tasks.Task.Delay(500); for (int i = 0; i < 3; i++) { bool ok = await NexusHTTPWebSocketClient.Instance.Connect(port); if (ok) { SynLog.Info($"[Synaptic] WebSocket re-attached to port {port} after reload."); return; } await System.Threading.Tasks.Task.Delay(1500); } SynLog.Warn($"[Synaptic] Could not reconnect WebSocket to port {port}. Use Setup window to reconnect manually."); } [InitializeOnLoadMethod] private static void CheckForUpdatesOnStartup() { // エディタ起動直後は少し待つ EditorApplication.delayCall += () => { // 1日1回チェック var lastCheck = EditorPrefs.GetString("SynapticPro_LastStartupUpdateCheck", ""); var today = DateTime.Now.ToString("yyyy-MM-dd"); if (lastCheck == today) return; EditorPrefs.SetString("SynapticPro_LastStartupUpdateCheck", today); var currentVersion = NexusVersion.Current; var dist = ENABLE_SELF_UPDATE ? "booth" : "assetstore"; System.Threading.ThreadPool.QueueUserWorkItem(_ => { try { using (var client = new System.Net.WebClient()) { client.Headers.Add("User-Agent", "SynapticAIPro-Unity"); var url = $"https://kawaii-agent-backend.vercel.app/api/synaptic/unity-version?v={currentVersion}&dist={dist}"; var json = client.DownloadString(url); if (json.Contains("\"updateAvailable\":true") || json.Contains("\"updateAvailable\": true")) { var vMatch = System.Text.RegularExpressions.Regex.Match(json, "\"latestVersion\"\\s*:\\s*\"([^\"]+)\""); if (vMatch.Success) { var newVersion = vMatch.Groups[1].Value; EditorApplication.delayCall += () => { var result = EditorUtility.DisplayDialog( "Synaptic AI Pro - Update Available", $"A new version is available!\n\n" + $"Current: v{currentVersion}\n" + $"Latest: v{newVersion}\n\n" + (ENABLE_SELF_UPDATE ? "Would you like to update now?" : "Would you like to open the Asset Store?"), ENABLE_SELF_UPDATE ? "Update Now" : "Open Store", "Later" ); if (result) { if (ENABLE_SELF_UPDATE) { // Setup Windowを開いてアップデート実行 var window = GetWindow("Synaptic Pro Setup"); window.Show(); updateAvailable = true; latestVersion = newVersion; var urlMatch = System.Text.RegularExpressions.Regex.Match(json, "\"updateUrl\"\\s*:\\s*\"([^\"]+)\""); updateUrl = urlMatch.Success ? urlMatch.Groups[1].Value : ""; updateMethod = "auto"; window.StartAutoUpdate(); } else { Application.OpenURL("https://assetstore.unity.com/packages/tools/generative-ai/synaptic-ai-pro-natural-language-control-for-unity-336030"); } } }; } } } } catch { /* フェイルサイレント */ } }); }; } private void TryHttpAutoStart() { if (externalAutoStartAttempted) return; externalAutoStartAttempted = true; if (HttpAutoStartEnabled && !externalHttpRunning) { // まず既存サーバーが生きてるかチェック if (IsPortListening(httpPort)) { // 既にサーバーが動いてる(前回のプロセスが残ってる)→ 再接続のみ externalHttpRunning = true; SynLog.Info("[Synaptic] Auto-start: Existing server detected, reconnecting..."); _ = ConnectToHttpServerAsync(httpPort); } else { // サーバーがない → 新規起動 SynLog.Info("[Synaptic] Auto-starting HTTP Server..."); StartExternalHttpServer(); } } } private void InitializeStyles() { headerStyle = new GUIStyle(EditorStyles.largeLabel) { fontSize = 24, fontStyle = FontStyle.Bold, alignment = TextAnchor.MiddleCenter, normal = { textColor = new Color(0.2f, 0.6f, 1f) } }; setupButtonStyle = new GUIStyle(GUI.skin.button) { fontSize = 18, fontStyle = FontStyle.Bold, padding = new RectOffset(20, 20, 10, 10), normal = { background = CreateColorTexture(new Color(0.2f, 0.6f, 1f)) }, hover = { background = CreateColorTexture(new Color(0.3f, 0.7f, 1f)) }, active = { background = CreateColorTexture(new Color(0.1f, 0.5f, 0.9f)) } }; statusStyle = new GUIStyle(EditorStyles.helpBox) { fontSize = 14, padding = new RectOffset(10, 10, 10, 10), wordWrap = true }; } private Texture2D CreateColorTexture(Color color) { var texture = new Texture2D(1, 1); texture.SetPixel(0, 0, color); texture.Apply(); return texture; } private void OnGUI() { if (headerStyle == null) InitializeStyles(); DrawHeader(); DrawUpdateBanner(); // Tab rendering selectedTab = GUILayout.Toolbar(selectedTab, tabNames, GUILayout.Height(30)); // Animation update (non-blocking) if (isConnecting) { animationTime += Time.deltaTime; // Use delayed call instead of direct Repaint to avoid "Hold on" blocking EditorApplication.delayCall += () => { if (this) Repaint(); }; } EditorGUILayout.Space(10); switch (selectedTab) { case 0: DrawAIConnectionTab(); break; case 1: DrawHTTPServerTab(); break; case 2: DrawHelpTab(); break; } } /// /// Compact toolbar with live MCP connection controls. Visible only in the /// AI Connection tab — the HTTP Server tab uses its own port lifecycle /// and these controls don't apply there. /// private void DrawConnectionControlsBar() { EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.BeginHorizontal(); // Status indicator: a small color-coded square + plain text label. // Using a textured box (drawn via a colored rect) instead of an // emoji bullet so the visual works under any system font. bool connected = NexusEditorMCPService.IsConnected; var statusColor = connected ? new Color(0.2f, 0.8f, 0.2f) : new Color(0.7f, 0.45f, 0.2f); var dotRect = GUILayoutUtility.GetRect(10, 10, GUILayout.Width(10), GUILayout.Height(10)); // Vertical-center the dot relative to surrounding label baseline. dotRect.y += 5; EditorGUI.DrawRect(dotRect, statusColor); GUILayout.Space(4); GUILayout.Label(connected ? "MCP Connected" : "MCP Disconnected", EditorStyles.boldLabel, GUILayout.Width(160)); GUILayout.FlexibleSpace(); // Built-in Editor icons — no emoji, render consistently across OS. // "Refresh" : circular arrow (used by Asset refresh, Console clear) // "linkicon" : chain link (Editor's link icon, varies by version) var reconnectIcon = EditorGUIUtility.IconContent("Refresh"); var reconnectContent = new GUIContent(" AI Reconnect", reconnectIcon.image, "Reconnect to MCP server"); if (GUILayout.Button(reconnectContent, GUILayout.Width(130), GUILayout.Height(22))) { NexusEditorMCPService.QuickReconnect(); } EditorGUILayout.Space(4); // Auto Reconnect toggle — direct binding to the EditorPrefs-backed property. bool prevAuto = NexusEditorMCPService.AutoReconnectEnabled; bool nextAuto = GUILayout.Toggle(prevAuto, new GUIContent(" Auto Reconnect", "Automatically reconnect when the connection drops"), GUILayout.Width(130)); if (nextAuto != prevAuto) { NexusEditorMCPService.AutoReconnectEnabled = nextAuto; } EditorGUILayout.Space(4); // No Unity built-in icon for Discord — use a small link/external // icon ("BuildSettings.Web.Small" is the closest globe-like icon // across Unity 2022 / 6.x). Fall back to plain text on failure. GUIContent discordContent; try { var icon = EditorGUIUtility.IconContent("BuildSettings.Web.Small"); discordContent = new GUIContent(" Discord", icon != null ? icon.image : null, "Join the Synaptic Discord community"); } catch { discordContent = new GUIContent("Discord", "Join the Synaptic Discord community"); } if (GUILayout.Button(discordContent, GUILayout.Width(90), GUILayout.Height(22))) { NexusEditorMCPService.OpenDiscord(); } EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); EditorGUILayout.Space(8); } private void DrawHeader() { EditorGUILayout.Space(10); GUILayout.Label("Synaptic Pro Setup", headerStyle, GUILayout.Height(40)); EditorGUILayout.Space(10); // Concise status display (常にBegin/Endを呼ぶ - GUILayout不一致防止) EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); var statusText = "Loading..."; var statusColor = Color.gray; if (mcpStatus != null) { if (mcpServerRunning) { statusText = "MCP Server Running"; statusColor = Color.green; } else if (mcpStatus.isMCPInstalled) { statusText = "AI Connection Ready"; statusColor = new Color(0.2f, 0.8f, 0.2f); } else { statusText = "Initial Setup"; statusColor = new Color(0.5f, 0.5f, 0.5f); } } var oldColor = GUI.contentColor; GUI.contentColor = statusColor; GUILayout.Label(statusText, EditorStyles.boldLabel); GUI.contentColor = oldColor; GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(5); } private void DrawAIConnectionTab() { scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); // ─── Live MCP connection controls ─────────────────────────────── // Previously the only entry points for these were Tools menu items // (Tools > Synaptic Pro > AI Reconnect / Auto Reconnect: Enable|Disable // / Join Discord). Surfacing them in the Setup window cuts the // discovery cost — users already open this window when troubleshooting. // MCP Server: Start/Stop stays in the Tools menu (advanced; the // typical workflow is to let Claude Desktop spawn the server). DrawConnectionControlsBar(); // One-click startup EditorGUILayout.BeginVertical(EditorStyles.helpBox); GUILayout.Label("MCP Setup", EditorStyles.boldLabel); EditorGUILayout.Space(5); EditorGUILayout.LabelField("Once MCP setup is complete, Unity tools are immediately available in Claude Desktop", EditorStyles.wordWrappedLabel); EditorGUILayout.Space(10); // Display connection animation if (isConnecting) { DrawConnectingAnimation(); } else if (!mcpConfigured) { // Setup not complete - show setup guide link EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Need help?", GUILayout.Width(70)); if (GUILayout.Button("Setup Guide", GUILayout.Width(100))) { Application.OpenURL("https://www.synaptic-ai.net/ja/docs/setup"); } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(10); // AI Tool Selection (v1.1.0) EditorGUILayout.LabelField("Select Your AI Tool:", EditorStyles.boldLabel); EditorGUILayout.Space(5); var oldBgColor = GUI.backgroundColor; // Token SuperSave Mode (Recommended) - TOP GUI.backgroundColor = selectedAIClient == AIClientType.TokenSuperSaveMode ? new Color(0.2f, 0.8f, 0.4f) : new Color(0.85f, 1f, 0.85f); if (GUILayout.Button( "★ Token SuperSave Mode [Recommended]\n" + "(3 meta-tools only - 99% context reduction)", GUILayout.Height(60))) { selectedAIClient = AIClientType.TokenSuperSaveMode; } EditorGUILayout.Space(5); // Full Mode option GUI.backgroundColor = selectedAIClient == AIClientType.ClaudeDesktopOrCursor ? new Color(0.3f, 0.7f, 0.9f) : Color.white; if (GUILayout.Button( "Full Mode\n" + "(All 350+ tools exposed)", GUILayout.Height(50))) { selectedAIClient = AIClientType.ClaudeDesktopOrCursor; } EditorGUILayout.Space(5); // Essential Mode option GUI.backgroundColor = selectedAIClient == AIClientType.CursorOrLMStudioEssential ? new Color(0.3f, 0.7f, 0.9f) : Color.white; if (GUILayout.Button( "Essential Mode\n" + "(80 essential tools)", GUILayout.Height(50))) { selectedAIClient = AIClientType.CursorOrLMStudioEssential; } EditorGUILayout.Space(5); // Dynamic Mode option GUI.backgroundColor = selectedAIClient == AIClientType.GitHubCopilot ? new Color(0.3f, 0.7f, 0.9f) : Color.white; if (GUILayout.Button( "Dynamic Mode\n" + "(8 core tools + on-demand loading)", GUILayout.Height(50))) { selectedAIClient = AIClientType.GitHubCopilot; } GUI.backgroundColor = oldBgColor; EditorGUILayout.Space(5); // Info box based on selection if (selectedAIClient == AIClientType.TokenSuperSaveMode) { EditorGUILayout.HelpBox( "★ Recommended: Token SuperSave Mode\n\n" + "Only 3 meta-tools for 99% context reduction:\n" + "• list_categories() - Discover tool categories\n" + "• list_tools(category) - See tools & parameters\n" + "• execute(tool, params) - Run any of 350+ tools\n\n" + "Works with all MCP clients. Best for long sessions.", MessageType.Info); } else if (selectedAIClient == AIClientType.ClaudeDesktopOrCursor) { EditorGUILayout.HelpBox( "Full Mode: All 350+ Unity tools loaded at startup.\n" + "• Higher context usage, but all tools immediately visible\n" + "• Claude Desktop: Prompt caching helps with longer sessions", MessageType.Info); } else if (selectedAIClient == AIClientType.CursorOrLMStudioEssential) { EditorGUILayout.HelpBox( "Essential Mode: 80 carefully selected tools (62% lighter).\n" + "• Perfect for Cursor and LM Studio to avoid context bloat\n" + "• Includes: GameObject, Camera, Scene, UI, Screenshot, Animation basics\n" + "• Removed: Scripting, GOAP, Weather, Advanced VFX, Batch ops", MessageType.Info); } else // GitHubCopilot { EditorGUILayout.HelpBox( "Dynamic Mode: Start with 8 tools, load more on-demand.\n" + "• GitHub Copilot with MCP support\n" + "• Use select_tools() to load additional tool categories\n" + "• Perfect for avoiding tool limit warnings", MessageType.Info); } EditorGUILayout.Space(10); // MCP Setup button var oldColor = GUI.backgroundColor; GUI.backgroundColor = new Color(0.2f, 0.6f, 0.8f); if (GUILayout.Button("Complete MCP Setup", setupButtonStyle, GUILayout.Height(60))) { ConfigureMCP(); } GUI.backgroundColor = oldColor; } else { // Setup complete EditorGUILayout.BeginHorizontal(); var oldColor = GUI.backgroundColor; GUI.backgroundColor = new Color(0.2f, 0.8f, 0.2f); GUILayout.Button("✓ MCP Setup Complete", setupButtonStyle, GUILayout.Height(50)); GUI.backgroundColor = oldColor; // Reconfigure button GUI.backgroundColor = new Color(0.8f, 0.6f, 0.2f); if (GUILayout.Button("🔄 Reconfigure", GUILayout.Width(100), GUILayout.Height(50))) { ResetMCPConfiguration(); } GUI.backgroundColor = oldColor; EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(10); // Display appropriate info based on selected mode (v1.1.0) string setupCompleteMessage; if (selectedAIClient == AIClientType.TokenSuperSaveMode) { setupCompleteMessage = "★ Setup complete! Token SuperSave Mode\n\n" + "• 3 meta-tools → 350+ tools accessible\n" + "• 99% context reduction for longer sessions\n\n" + "Restart your AI tool, then ask:\n" + "\"What Unity tools are available?\""; } else if (selectedAIClient == AIClientType.GitHubCopilot) { setupCompleteMessage = "Setup complete! Dynamic Mode (hub-server.js)\n" + "• GitHub Copilot (.vscode/mcp.json)\n\n" + "Restart VS Code to activate.\n" + "Use select_tools() to load tool categories dynamically."; } else { setupCompleteMessage = "Setup complete! Full Mode (index.js) - All 246 tools\n" + "• Claude Desktop\n" + "• Cursor (~/.cursor/mcp.json)\n" + "• VS Code (.vscode/mcp.json)\n\n" + "Restart/Reload your AI tool to activate Unity MCP."; } EditorGUILayout.HelpBox(setupCompleteMessage, MessageType.Info); } EditorGUILayout.EndVertical(); EditorGUILayout.Space(20); // How to Use EditorGUILayout.BeginVertical(EditorStyles.helpBox); GUILayout.Label("How to Use Unity Tools", EditorStyles.boldLabel); EditorGUILayout.Space(5); EditorGUILayout.LabelField("1. Open Claude Desktop / Cursor / VS Code", EditorStyles.wordWrappedLabel); EditorGUILayout.LabelField("2. Ask: \"What Unity tools are available?\"", EditorStyles.wordWrappedLabel); EditorGUILayout.LabelField("3. Give instructions: \"Create a red cube\" or \"Show me the scene hierarchy\"", EditorStyles.wordWrappedLabel); EditorGUILayout.Space(10); if (GUILayout.Button("Full Usage Guide", GUILayout.Height(35))) { ShowUsageGuide(); } EditorGUILayout.EndVertical(); // Auto-generated connection settings if (mcpServerRunning) { EditorGUILayout.Space(20); EditorGUILayout.BeginVertical(EditorStyles.helpBox); GUILayout.Label("Connection Settings (Auto-generated)", EditorStyles.boldLabel); EditorGUILayout.Space(5); EditorGUILayout.LabelField("MCP Server: localhost:8090"); EditorGUILayout.LabelField("Tool Name: unity-synaptic"); EditorGUILayout.Space(10); EditorGUILayout.LabelField("💫 Usage:", EditorStyles.boldLabel); EditorGUILayout.LabelField("1. Open ChatGPT or Claude Desktop"); EditorGUILayout.LabelField("2. Start a new chat"); EditorGUILayout.LabelField("3. Tips for using tools:"); EditorGUILayout.LabelField(" • Include words like \"tools\" or \"unity\""); EditorGUILayout.LabelField(" Example: \"Use unity tools to create a red cube\""); EditorGUILayout.LabelField("4. AI will automatically control Unity with MCP tools"); EditorGUILayout.EndVertical(); } EditorGUILayout.EndScrollView(); } private void DrawServerManagementTab() { EditorGUILayout.BeginVertical(EditorStyles.helpBox); GUILayout.Label("MCP Server Management", EditorStyles.boldLabel); EditorGUILayout.Space(10); // Server status EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Server Status:"); var statusColor = mcpServerRunning ? Color.green : Color.red; var statusText = mcpServerRunning ? "● Running" : "● Stopped"; var oldColor = GUI.contentColor; GUI.contentColor = statusColor; GUILayout.Label(statusText); GUI.contentColor = oldColor; GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(10); // Control buttons EditorGUILayout.BeginHorizontal(); EditorGUI.BeginDisabledGroup(!mcpStatus?.isMCPInstalled ?? true); if (!mcpServerRunning) { if (GUILayout.Button("▶️ Start Server", GUILayout.Height(30))) { StartMCPServer(); } } else { if (GUILayout.Button("⏹️ Stop Server", GUILayout.Height(30))) { StopMCPServer(); } } if (GUILayout.Button("🔄 Restart", GUILayout.Height(30))) { RestartMCPServer(); } EditorGUI.EndDisabledGroup(); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(20); // Server settings EditorGUILayout.Space(20); EditorGUILayout.LabelField("Server Settings:", EditorStyles.boldLabel); EditorGUI.indentLevel++; EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("MCP Port:", GUILayout.Width(100)); mcpPort = EditorGUILayout.IntField(mcpPort); EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("WebSocket Port:", GUILayout.Width(100)); wsPort = EditorGUILayout.IntField(wsPort); EditorGUILayout.EndHorizontal(); EditorGUI.indentLevel--; // Connection Info if (mcpStatus?.isMCPInstalled ?? false) { EditorGUILayout.Space(10); EditorGUILayout.LabelField("Connection Info:", EditorStyles.boldLabel); EditorGUI.indentLevel++; EditorGUILayout.LabelField($"MCP: localhost:{mcpPort}"); EditorGUILayout.LabelField($"WebSocket: ws://localhost:{wsPort}"); EditorGUILayout.LabelField($"Accessible from Desktop AI"); EditorGUI.indentLevel--; } EditorGUILayout.EndVertical(); // Log Viewer EditorGUILayout.Space(20); EditorGUILayout.BeginVertical(EditorStyles.helpBox); GUILayout.Label("Server Log", EditorStyles.boldLabel); // Display server logs here EditorGUILayout.TextArea("Server logs will be displayed here...", GUILayout.Height(200)); EditorGUILayout.EndVertical(); } /* Planned for future implementation private void DrawCLIConfigTab() { scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); EditorGUILayout.BeginVertical(EditorStyles.helpBox); GUILayout.Label("CLI AI Configuration Manager", EditorStyles.boldLabel); EditorGUILayout.Space(10); EditorGUILayout.LabelField("Batch create configuration files for various CLI AI tools", EditorStyles.wordWrappedLabel); EditorGUILayout.Space(10); // Claude Code configuration (2025 MCP specification) EditorGUILayout.BeginVertical(EditorStyles.helpBox); GUILayout.Label("Claude Code (Anthropic)", EditorStyles.boldLabel); EditorGUILayout.LabelField("Official CLI for Claude with MCP support", EditorStyles.miniLabel); EditorGUILayout.Space(5); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Configure MCP", GUILayout.Height(30))) { GenerateClaudeCodeConfig(); } if (GUILayout.Button("Docs", GUILayout.Width(60), GUILayout.Height(30))) { Application.OpenURL("https://docs.anthropic.com/en/docs/claude-code/mcp"); } EditorGUILayout.EndHorizontal(); EditorGUILayout.LabelField("Config: .claude/settings.local.json", EditorStyles.miniLabel); EditorGUILayout.EndVertical(); EditorGUILayout.Space(10); // Cursor settings (2025 Popular MCP Client) EditorGUILayout.BeginVertical(EditorStyles.helpBox); GUILayout.Label("Cursor (Anysphere)", EditorStyles.boldLabel); EditorGUILayout.LabelField("AI-powered code editor with MCP support", EditorStyles.miniLabel); EditorGUILayout.Space(5); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Configure MCP", GUILayout.Height(30))) { if (GenerateCursorConfig()) { EditorUtility.DisplayDialog( "Cursor Setup Complete", "Cursor MCP configuration created successfully!\n\n" + "Config file: ~/.cursor/mcp.json\n\n" + "Next steps:\n" + "1. Reload MCP Servers in Cursor:\n" + " Settings → MCP Servers → Reload\n" + "2. Unity tools will be available in Cursor", "OK" ); } else { EditorUtility.DisplayDialog( "Cursor Setup Failed", "Failed to create Cursor configuration.\n\n" + "Please check the Console for details.", "OK" ); } } if (GUILayout.Button("Docs", GUILayout.Width(60), GUILayout.Height(30))) { Application.OpenURL("https://www.synaptic-ai.net/ja/docs/setup#cursor"); } EditorGUILayout.EndHorizontal(); EditorGUILayout.LabelField("Config: ~/.cursor/mcp.json", EditorStyles.miniLabel); EditorGUILayout.EndVertical(); EditorGUILayout.Space(10); // VS Code settings (Claude Code extension) EditorGUILayout.BeginVertical(EditorStyles.helpBox); GUILayout.Label("VS Code (Microsoft)", EditorStyles.boldLabel); EditorGUILayout.LabelField("Visual Studio Code with Claude Code extension MCP support", EditorStyles.miniLabel); EditorGUILayout.Space(5); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Configure MCP", GUILayout.Height(30))) { if (GenerateVSCodeConfig()) { EditorUtility.DisplayDialog( "VS Code Setup Complete", "VS Code MCP configuration created successfully!\n\n" + "Config file: .vscode/mcp.json\n\n" + "Next steps:\n" + "1. Reload VS Code window:\n" + " Cmd/Ctrl + Shift + P → 'Reload Window'\n" + "2. Unity tools will be available in Claude Code", "OK" ); } else { EditorUtility.DisplayDialog( "VS Code Setup Failed", "Failed to create VS Code configuration.\n\n" + "Please check the Console for details.", "OK" ); } } if (GUILayout.Button("Docs", GUILayout.Width(60), GUILayout.Height(30))) { Application.OpenURL("https://www.synaptic-ai.net/ja/docs/setup#vscode"); } EditorGUILayout.EndHorizontal(); EditorGUILayout.LabelField("Config: .vscode/mcp.json", EditorStyles.miniLabel); EditorGUILayout.EndVertical(); EditorGUILayout.Space(10); // Windsurf settings (2025 MCP Client) EditorGUILayout.BeginVertical(EditorStyles.helpBox); GUILayout.Label("Windsurf (Codeium)", EditorStyles.boldLabel); EditorGUILayout.LabelField("The IDE that writes with you - MCP enabled", EditorStyles.miniLabel); EditorGUILayout.Space(5); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Configure MCP", GUILayout.Height(30))) { GenerateWindsurfConfig(); } if (GUILayout.Button("Docs", GUILayout.Width(60), GUILayout.Height(30))) { Application.OpenURL("https://codeium.com/windsurf"); } EditorGUILayout.EndHorizontal(); EditorGUILayout.LabelField("Config: ~/.windsurf/mcp_servers.json", EditorStyles.miniLabel); EditorGUILayout.EndVertical(); EditorGUILayout.Space(20); // Batch settings EditorGUILayout.BeginVertical(EditorStyles.helpBox); GUILayout.Label("Batch Configuration", EditorStyles.boldLabel); EditorGUILayout.Space(5); EditorGUILayout.LabelField("Configure all supported MCP clients at once", EditorStyles.wordWrappedLabel); EditorGUILayout.Space(10); var oldColor = GUI.backgroundColor; GUI.backgroundColor = new Color(0.2f, 0.8f, 0.2f); if (GUILayout.Button("Configure All MCP Clients", GUILayout.Height(40))) { GenerateAllCLIConfigs(); } GUI.backgroundColor = oldColor; EditorGUILayout.EndVertical(); EditorGUILayout.Space(20); // File watch settings EditorGUILayout.BeginVertical(EditorStyles.helpBox); GUILayout.Label("File Watch Settings", EditorStyles.boldLabel); EditorGUILayout.Space(5); EditorGUILayout.LabelField("Monitor project file changes and notify AI", EditorStyles.wordWrappedLabel); EditorGUILayout.Space(10); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Create Watch Configuration", GUILayout.Height(30))) { GenerateWatchConfig(); } EditorGUILayout.EndHorizontal(); EditorGUILayout.LabelField("Monitored files: .cs, .js, .ts, .json, .md", EditorStyles.miniLabel); EditorGUILayout.EndVertical(); EditorGUILayout.EndVertical(); EditorGUILayout.EndScrollView(); } */ private void DrawHTTPServerTab() { scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); // HTTP Server Section EditorGUILayout.BeginVertical(EditorStyles.helpBox); GUILayout.Label("HTTP Server", EditorStyles.boldLabel); EditorGUILayout.Space(5); EditorGUILayout.HelpBox( "HTTP Server allows direct API access to Unity tools.\n" + "Use this for custom integrations, testing, or when MCP is not available.", MessageType.Info); EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); if (GUILayout.Button("CLI Integration Guide", GUILayout.Width(150))) { Application.OpenURL("https://www.synaptic-ai.net/ja/docs/cli-integration"); } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(10); // External HTTP Server UI DrawExternalHTTPServerUI(); EditorGUILayout.EndVertical(); // API Endpoints Section httpServerRunning = externalHttpRunning; if (httpServerRunning) { EditorGUILayout.Space(15); EditorGUILayout.BeginVertical(EditorStyles.helpBox); GUILayout.Label("API Endpoints", EditorStyles.boldLabel); EditorGUILayout.Space(5); var baseUrl = $"http://localhost:{httpPort}"; DrawEndpointRow("Root (Prompt)", "GET", $"{baseUrl}/"); DrawEndpointRow("Health Check", "GET", $"{baseUrl}/health"); DrawEndpointRow("AI Prompt", "GET", $"{baseUrl}/prompt"); DrawEndpointRow("List Tools", "GET", $"{baseUrl}/tools"); DrawEndpointRow("Categories", "GET", $"{baseUrl}/categories"); DrawEndpointRow("Tool Search", "GET", $"{baseUrl}/tools/search?q="); DrawEndpointRow("Tools Reference", "GET", $"{baseUrl}/tools/reference"); DrawEndpointRow("Resources", "GET", $"{baseUrl}/resources"); DrawEndpointRow("Execute Tool", "POST", $"{baseUrl}/execute"); DrawEndpointRow("Batch Execute", "POST", $"{baseUrl}/batch"); EditorGUILayout.Space(10); if (GUILayout.Button("Copy Base URL", GUILayout.Height(25))) { GUIUtility.systemCopyBuffer = baseUrl; SynLog.Info($"Copied: {baseUrl}"); } EditorGUILayout.EndVertical(); } // Usage Example EditorGUILayout.Space(15); EditorGUILayout.BeginVertical(EditorStyles.helpBox); GUILayout.Label("Usage Example", EditorStyles.boldLabel); EditorGUILayout.Space(5); EditorGUILayout.LabelField("cURL Example:", EditorStyles.boldLabel); var curlExample = $"curl http://localhost:{httpPort}/health"; EditorGUILayout.SelectableLabel(curlExample, EditorStyles.textField, GUILayout.Height(20)); EditorGUILayout.Space(5); EditorGUILayout.LabelField("Execute Tool (Recommended):", EditorStyles.boldLabel); var toolExample = $"curl -X POST http://localhost:{httpPort}/execute \\\n -H \"Content-Type: application/json\" \\\n -d '{{\"tool\": \"unity_create_gameobject\", \"params\": {{\"name\": \"MyCube\", \"type\": \"cube\"}}}}'"; EditorGUILayout.SelectableLabel(toolExample, EditorStyles.textArea, GUILayout.Height(50)); EditorGUILayout.Space(5); EditorGUILayout.LabelField("Batch Execute:", EditorStyles.boldLabel); var batchExample = $"curl -X POST http://localhost:{httpPort}/batch \\\n -H \"Content-Type: application/json\" \\\n -d '[{{\"tool\": \"unity_create_gameobject\", \"params\": {{\"name\": \"Obj1\", \"type\": \"cube\"}}}}, ...]'"; EditorGUILayout.SelectableLabel(batchExample, EditorStyles.textArea, GUILayout.Height(50)); EditorGUILayout.EndVertical(); // AI Prompt Section EditorGUILayout.Space(15); EditorGUILayout.BeginVertical(EditorStyles.helpBox); GUILayout.Label("AI Control Prompt", EditorStyles.boldLabel); EditorGUILayout.Space(5); EditorGUILayout.HelpBox( "Copy this prompt to your AI (Claude Code, Codex CLI, etc.) to enable HTTP control.\n" + $"Or fetch directly: curl http://localhost:{httpPort}/", MessageType.Info); EditorGUILayout.Space(10); if (GUILayout.Button("Copy AI Prompt to Clipboard", GUILayout.Height(30))) { var mcpServerPath = FindMCPServerPath(); var aiPrompt = GetHTTPControlPrompt(mcpServerPath, httpPort); GUIUtility.systemCopyBuffer = aiPrompt; SynLog.Info("[Synaptic] AI Prompt copied to clipboard!"); EditorUtility.DisplayDialog("Copied!", "AI Control Prompt has been copied to clipboard.\n\nPaste it to your AI assistant to enable HTTP control.", "OK"); } EditorGUILayout.EndVertical(); // ========== ログ出力設定 ========== EditorGUILayout.Space(15); EditorGUILayout.BeginVertical(EditorStyles.helpBox); GUILayout.Label("Log Output", EditorStyles.boldLabel); EditorGUILayout.Space(5); bool currentVerbose = SynapticAIPro.SynLog.VerboseEnabled; bool newVerbose = EditorGUILayout.ToggleLeft( "Verbose Logs (Info / Warning)", currentVerbose ); if (newVerbose != currentVerbose) { SynapticAIPro.SynLog.VerboseEnabled = newVerbose; } EditorGUILayout.LabelField( "オフにすると Synaptic AI Pro 関連の Info/Warning ログをコンソールに出力しません(Errorは常に表示)", EditorStyles.wordWrappedMiniLabel ); EditorGUILayout.EndVertical(); EditorGUILayout.EndScrollView(); } private double lastHttpStatusCheckTime = 0; private void DrawExternalHTTPServerUI() { // Server Status(5秒ごとにチェック。毎フレームのTCP接続を防ぐ) if (EditorApplication.timeSinceStartup - lastHttpStatusCheckTime > 5.0) { lastHttpStatusCheckTime = EditorApplication.timeSinceStartup; CheckExternalHttpServerStatus(); } EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Status:", GUILayout.Width(60)); var statusColor = externalHttpRunning ? Color.green : Color.gray; var statusText = externalHttpRunning ? "● Running (Node.js)" : "● Stopped"; var oldColor = GUI.contentColor; GUI.contentColor = statusColor; GUILayout.Label(statusText, EditorStyles.boldLabel); GUI.contentColor = oldColor; GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(5); // Port Setting EditorGUI.BeginDisabledGroup(externalHttpRunning); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("HTTP Port:", GUILayout.Width(80)); httpPort = EditorGUILayout.IntField(httpPort, GUILayout.Width(80)); GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); EditorGUI.EndDisabledGroup(); EditorGUILayout.Space(5); // Auto-Start Toggle EditorGUILayout.BeginHorizontal(); var autoStart = HttpAutoStartEnabled; var newAutoStart = EditorGUILayout.Toggle("Auto-Start on Load", autoStart); if (newAutoStart != autoStart) { HttpAutoStartEnabled = newAutoStart; } GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(10); // Control Buttons EditorGUILayout.BeginHorizontal(); if (!externalHttpRunning) { var startBgColor = GUI.backgroundColor; GUI.backgroundColor = new Color(0.2f, 0.7f, 0.3f); if (GUILayout.Button("Start HTTP Server", GUILayout.Height(35))) { StartExternalHttpServer(); } GUI.backgroundColor = startBgColor; } else { var stopBgColor = GUI.backgroundColor; GUI.backgroundColor = new Color(0.8f, 0.3f, 0.3f); if (GUILayout.Button("Stop HTTP Server", GUILayout.Height(35))) { StopExternalHttpServer(); } GUI.backgroundColor = stopBgColor; } EditorGUILayout.EndHorizontal(); // Connect status / Connect button EditorGUILayout.Space(5); var wsConnected = NexusHTTPWebSocketClient.Instance?.IsConnected ?? false; // GUILayout構造を常に同じにする(Layout/Repaintフェーズ不一致防止) EditorGUI.BeginDisabledGroup(wsConnected); if (GUILayout.Button( wsConnected ? $"✓ Unity connected (port {httpPort})" : "Connect Unity Only (Server already running)", GUILayout.Height(28))) { if (!wsConnected) { _ = ConnectToHttpServerAsync(httpPort); } } EditorGUI.EndDisabledGroup(); // Info about external mode EditorGUILayout.Space(5); EditorGUILayout.HelpBox( "External mode runs http-server.js via Node.js.\n" + "Benefits: Stable across domain reloads, Play mode changes.\n" + "HTTP and WebSocket use the same port.", MessageType.None); } // ===== External HTTP Server Process Management ===== private void CheckExternalHttpServerStatus() { // Check if process is still running if (externalHttpProcess != null) { try { if (externalHttpProcess.HasExited) { externalHttpProcess = null; externalHttpRunning = false; } else { externalHttpRunning = true; return; } } catch { externalHttpProcess = null; externalHttpRunning = false; } } // No local process handle (fresh domain reload, or never-started). // Probe the port on a background thread and cache the result. OnGUI must // never block on a TCP connect — a 500ms stall per repaint freezes the editor. if (portCheckInFlight) return; double now = EditorApplication.timeSinceStartup; if (now - lastPortCheckTime < PORT_CHECK_INTERVAL_SEC) return; lastPortCheckTime = now; portCheckInFlight = true; int portToCheck = httpPort; _ = Task.Run(async () => { bool listening = false; try { using (var tcp = new System.Net.Sockets.TcpClient()) { var connectTask = tcp.ConnectAsync("localhost", portToCheck); var winner = await Task.WhenAny(connectTask, Task.Delay(PORT_CHECK_TIMEOUT_MS)); if (winner == connectTask) { try { await connectTask; listening = tcp.Connected; } catch { listening = false; } } } } catch { listening = false; } // Marshal state + Repaint back to the main thread. EditorApplication.delayCall += () => { portCheckInFlight = false; if (externalHttpRunning != listening) { externalHttpRunning = listening; if (this) Repaint(); } }; }); } private bool IsPortListening(int port) { // Quick TCP check with short timeout (avoid blocking UI thread) try { using (var tcp = new System.Net.Sockets.TcpClient()) { var result = tcp.BeginConnect("localhost", port, null, null); bool connected = result.AsyncWaitHandle.WaitOne(500); // 500msタイムアウト if (connected && tcp.Connected) { tcp.EndConnect(result); return true; } return false; } } catch { return false; } } private void StartExternalHttpServer() { var mcpServerPath = FindMCPServerPath(); var httpServerScript = Path.Combine(mcpServerPath, "http-server.js"); if (!File.Exists(httpServerScript)) { EditorUtility.DisplayDialog("Error", $"http-server.js not found at:\n{httpServerScript}\n\nPlease ensure Synaptic AI Pro is properly installed.", "OK"); return; } // Find Node.js path var nodePath = FindNodePath(); if (string.IsNullOrEmpty(nodePath)) { EditorUtility.DisplayDialog("Error", "Node.js not found.\n\nPlease install Node.js from https://nodejs.org/", "OK"); return; } try { System.Diagnostics.ProcessStartInfo startInfo; if (Application.platform == RuntimePlatform.WindowsEditor) { // Windows: detached from Unity's Job Object via CreateProcessW // with CREATE_BREAKAWAY_FROM_JOB. Process.Start inherits the // Job and gets killed on assembly reload — see ESC-0095. string logDir = Path.Combine(mcpServerPath, "logs"); try { Directory.CreateDirectory(logDir); } catch { } string logFile = Path.Combine(logDir, "http-server.log"); int pid = SynapticDetachedProcess.StartWindows( nodePath, httpServerScript, httpPort, mcpServerPath, logFile); if (pid == 0) { SynLog.Info("[Synaptic] Detached spawn failed, falling back to Process.Start"); // Fall through to legacy path below startInfo = new System.Diagnostics.ProcessStartInfo { FileName = nodePath, Arguments = $"\"{httpServerScript}\" {httpPort}", WorkingDirectory = mcpServerPath, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, StandardOutputEncoding = System.Text.Encoding.UTF8, StandardErrorEncoding = System.Text.Encoding.UTF8 }; startInfo.EnvironmentVariables["HTTP_PORT"] = httpPort.ToString(); } else { // Detached path: process is independent of Unity's Job. // We don't keep externalHttpProcess (no handle) — recovery // uses SessionState PID. Mark running and connect. externalHttpProcess = null; externalHttpRunning = true; EditorPrefs.SetInt(PREF_HTTP_PORT, httpPort); SynLog.Info($"[Synaptic] External HTTP Server started detached (PID={pid}) on port {httpPort}"); SynLog.Info($"[Synaptic] Node: {nodePath}"); SynLog.Info($"[Synaptic] Script: {httpServerScript}"); SynLog.Info($"[Synaptic] Log: {logFile}"); _ = ConnectToHttpServerAsync(httpPort); return; } } else { // macOS/Linux: 直接起動 // // Previous implementation piped stdout/stderr back to C# // (RedirectStandardOutput/Error + BeginOutputReadLine). // When Unity's C# domain reloads (recompile), the pipe // readers on the C# side disappear; the node process's // next stdout write then hits SIGPIPE and node terminates. // Result: HTTP server died every time a script was edited. // // Fix: launch via `sh -c "nohup node ... >log 2>&1 &"` so // the child is fully detached from Unity's pipes and // process group, mirroring the Windows detached path. string logDir = Path.Combine(mcpServerPath, "logs"); try { Directory.CreateDirectory(logDir); } catch { } string logFile = Path.Combine(logDir, "http-server.log"); // sh -c is portable to both macOS (BSD sh) and Linux. // Quote the script path; quote the log path; redirect both // streams to the log file; trailing `&` to detach. string escapedScript = httpServerScript.Replace("\"", "\\\""); string escapedLog = logFile.Replace("\"", "\\\""); string shellCmd = $"nohup \"{nodePath}\" \"{escapedScript}\" {httpPort} " + $">\"{escapedLog}\" 2>&1 { if (!string.IsNullOrEmpty(e.Data)) SynLog.Info($"[HTTP Server] {e.Data}"); }; externalHttpProcess.ErrorDataReceived += (sender, e) => { if (!string.IsNullOrEmpty(e.Data)) SynLog.Info($"[HTTP Server] {e.Data}"); }; externalHttpProcess.Start(); externalHttpProcess.BeginOutputReadLine(); externalHttpProcess.BeginErrorReadLine(); externalHttpRunning = true; EditorPrefs.SetInt(PREF_HTTP_PORT, httpPort); SynLog.Info($"[Synaptic] External HTTP Server started on port {httpPort}"); SynLog.Info($"[Synaptic] Node: {nodePath}"); SynLog.Info($"[Synaptic] Script: {httpServerScript}"); // Connect Unity to HTTP Server via WebSocket (with delay to let server start) _ = ConnectToHttpServerAsync(httpPort); } catch (Exception e) { Debug.LogError($"[Synaptic] Failed to start external HTTP server: {e.Message}"); EditorUtility.DisplayDialog("Error", $"Failed to start HTTP server:\n{e.Message}", "OK"); externalHttpProcess = null; externalHttpRunning = false; } } private async Task ConnectToHttpServerAsync(int port) { // Wait for HTTP Server to start (Windows needs more time via cmd.exe) var waitTime = Application.platform == RuntimePlatform.WindowsEditor ? 3000 : 1500; await Task.Delay(waitTime); // Retry connection with increasing delays for (int i = 0; i < 3; i++) { var connected = await NexusHTTPWebSocketClient.Instance.Connect(port); if (connected) { SynLog.Info($"[Synaptic] Unity connected to HTTP Server on port {port}"); return; } SynLog.Info($"[Synaptic] Connection attempt {i + 1}/3 failed, retrying..."); await Task.Delay(2000); } SynLog.Warn($"[Synaptic] Failed to connect Unity to HTTP Server after 3 attempts.\nTry starting manually: cd to MCPServer folder and run 'node http-server.js {port}'"); } private void StopExternalHttpServer() { // Detached path: kill by stored PID (no Process handle exists) if (Application.platform == RuntimePlatform.WindowsEditor && externalHttpProcess == null && externalHttpRunning) { if (SynapticDetachedProcess.KillStored()) { externalHttpRunning = false; SynLog.Info("[Synaptic] Detached HTTP server stopped."); return; } } // Kill process first (WebSocket will disconnect automatically) if (externalHttpProcess != null) { try { if (!externalHttpProcess.HasExited) { externalHttpProcess.Kill(); externalHttpProcess.WaitForExit(3000); } externalHttpProcess.Dispose(); } catch (Exception e) { SynLog.Warn($"[Synaptic] Error stopping HTTP server: {e.Message}"); } finally { externalHttpProcess = null; externalHttpRunning = false; } } else if (externalHttpRunning) { // Process reference lost (e.g., after domain reload) // Kill by port number instead KillProcessOnPortSafe(httpPort); externalHttpRunning = false; SynLog.Info($"[Synaptic] HTTP Server on port {httpPort} - process reference lost. Killing by port."); } SynLog.Info("[Synaptic] External HTTP Server stopped"); } private void KillProcessOnPortSafe(int port) { // Run in a separate thread to avoid blocking Unity System.Threading.ThreadPool.QueueUserWorkItem(_ => { try { KillProcessOnPort(port); } catch (Exception e) { SynLog.Warn($"[Synaptic] Could not kill process on port {port}: {e.Message}"); } }); } private void KillProcessOnPort(int port) { try { if (Application.platform == RuntimePlatform.OSXEditor) { // macOS: lsof + kill var lsofProcess = new System.Diagnostics.Process { StartInfo = new System.Diagnostics.ProcessStartInfo { FileName = "/bin/bash", Arguments = $"-c \"lsof -ti :{port} | xargs kill -9 2>/dev/null\"", UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true } }; lsofProcess.Start(); lsofProcess.WaitForExit(3000); } else if (Application.platform == RuntimePlatform.WindowsEditor) { // Windows: netstat + taskkill var process = new System.Diagnostics.Process { StartInfo = new System.Diagnostics.ProcessStartInfo { FileName = "cmd.exe", Arguments = $"/c \"for /f \"tokens=5\" %a in ('netstat -aon ^| findstr :{port} ^| findstr LISTENING') do taskkill /PID %a /F\"", UseShellExecute = false, CreateNoWindow = true } }; process.Start(); process.WaitForExit(3000); } } catch (Exception e) { SynLog.Warn($"[Synaptic] Failed to kill process on port {port}: {e.Message}"); } } private string GetHTTPControlPrompt(string mcpServerPath, int port) { return $@"# Synaptic AI Pro (Unity) HTTP Control Instructions ## Prerequisites - Unity must be open with Synaptic AI Pro project loaded - HTTP Server is running (started from this window or auto-start enabled) - No external processes needed - server runs inside Unity ## Endpoints - GET /health - Server status - GET /prompt - Get this AI control prompt - GET /categories - List all tool categories - GET /tools/category/:cat - List tools in category with inputSchema - GET /tools - Full tool registry - POST /execute - Execute single tool (RECOMMENDED) - POST /batch - Execute multiple tools at once (RECOMMENDED) - POST /tool/:toolName - Execute tool (legacy) ## Verify connection curl http://localhost:{port}/health ## Tool discovery curl http://localhost:{port}/categories curl http://localhost:{port}/tools/category/scene ## Single tool execution (RECOMMENDED) curl -X POST http://localhost:{port}/execute -H ""Content-Type: application/json"" -d '{{""tool"":""unity_create_gameobject"",""params"":{{""name"":""MyCube"",""type"":""cube""}}}}' ## Batch execution (RECOMMENDED for multiple operations) curl -X POST http://localhost:{port}/batch -H ""Content-Type: application/json"" -d '[ {{""tool"":""unity_create_gameobject"",""params"":{{""name"":""Cube1"",""type"":""cube""}}}}, {{""tool"":""unity_set_transform"",""params"":{{""name"":""Cube1"",""position"":""2,0,0""}}}}, {{""tool"":""unity_create_gameobject"",""params"":{{""name"":""Sphere1"",""type"":""sphere""}}}} ]' ## If connection fails - Connection refused → Unity not open or HTTP Server not started - Start HTTP Server from: Tools > Synaptic Pro > Synaptic Setup > HTTP Server tab - Or enable ""Auto-Start on Load"" for automatic startup ## Note for Codex CLI CRITICAL: Run curl commands EXACTLY as shown. Do NOT add nice, nohup, redirects (>), or other modifications. If you get ""nice(5) failed"" or similar sandbox errors, use escalation (request additional permission from user). Sandbox may block localhost connections. Use escalation if curl commands fail. ## Notes - All responses are JSON - Use /batch for multiple operations (more efficient) - 30 second timeout per request "; } private void DrawEndpointRow(string name, string method, string url) { EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField(name, GUILayout.Width(100)); EditorGUILayout.LabelField(method, EditorStyles.miniLabel, GUILayout.Width(40)); EditorGUILayout.SelectableLabel(url, EditorStyles.textField, GUILayout.Height(18)); EditorGUILayout.EndHorizontal(); } private void DrawHelpTab() { scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); EditorGUILayout.BeginVertical(EditorStyles.helpBox); GUILayout.Label("AI Connection Help", EditorStyles.boldLabel); EditorGUILayout.Space(10); // Simple description DrawHelpSection("What is Synaptic Pro?", "A tool that connects Unity with desktop AI apps like Claude, Gemini, and more.\n" + "No complex setup required - get started with one click."); // How to use DrawHelpSection("How to Use", "1. Click 'Start AI Connection' in the AI Connection tab\n" + "2. Open Claude/Gemini desktop app\n" + "3. Connect to localhost:3000 in AI app settings\n" + "4. AI is now available in Unity!"); // Supported AI apps DrawHelpSection("Supported AI Apps", "• Claude Desktop (Recommended)\n" + "• Cursor / Windsurf\n" + "• VS Code (GitHub Copilot)\n" + "• Gemini CLI / Codex CLI\n" + "• Other MCP-compatible AI apps"); // Troubleshooting DrawHelpSection("When Things Don't Work", "• Error when clicking 'Start AI Connection'\n" + " → Run Unity as administrator\n\n" + "• Cannot connect from AI app\n" + " → Check firewall settings\n\n" + "• Connection succeeds but cannot control Unity\n" + " → Confirm 'Start AI Connection' is pressed in Unity"); // Links EditorGUILayout.Space(20); GUILayout.Label("Documentation", EditorStyles.boldLabel); if (GUILayout.Button("Setup Guide")) { Application.OpenURL("https://www.synaptic-ai.net/ja/docs/setup"); } if (GUILayout.Button("CLI Integration")) { Application.OpenURL("https://www.synaptic-ai.net/ja/docs/cli-integration"); } if (GUILayout.Button("Discord Community")) { Application.OpenURL("https://discord.gg/MXwHCVWmPe"); } EditorGUILayout.EndVertical(); EditorGUILayout.EndScrollView(); } private void DrawStatusItem(string label, bool isInstalled, string version) { EditorGUILayout.BeginHorizontal(); var icon = isInstalled ? "✅" : "❌"; var color = isInstalled ? Color.green : Color.red; var oldColor = GUI.contentColor; GUI.contentColor = color; GUILayout.Label(icon, GUILayout.Width(20)); GUI.contentColor = oldColor; EditorGUILayout.LabelField(label, GUILayout.Width(100)); if (!string.IsNullOrEmpty(version)) { EditorGUILayout.LabelField(version, EditorStyles.miniLabel); } GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); } private void DrawHelpSection(string title, string content) { GUILayout.Label(title, EditorStyles.boldLabel); EditorGUI.indentLevel++; EditorGUILayout.LabelField(content, EditorStyles.wordWrappedLabel); EditorGUI.indentLevel--; EditorGUILayout.Space(10); } private void DrawConnectingAnimation() { // Animation box EditorGUILayout.BeginVertical(EditorStyles.helpBox); // Rotating spinner var spinnerChars = new string[] { "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" }; var spinnerIndex = (int)(animationTime * 10) % spinnerChars.Length; // Current message (fixed to avoid looping) var messageIndex = Mathf.Min((int)(animationTime / 3), connectingMessages.Length - 1); var currentMessage = connectingMessages[messageIndex]; // Animation display var animatedText = $"{spinnerChars[spinnerIndex]} {currentMessage}..."; var centeredStyle = new GUIStyle(EditorStyles.boldLabel) { alignment = TextAnchor.MiddleCenter, fontSize = 16 }; GUILayout.Label(animatedText, centeredStyle, GUILayout.Height(60)); // Progress bar (100% at the last message) var progress = (messageIndex + 1f) / connectingMessages.Length; var rect = GUILayoutUtility.GetRect(0, 4, GUILayout.ExpandWidth(true)); EditorGUI.ProgressBar(rect, progress, ""); // Cancel button EditorGUILayout.Space(10); if (GUILayout.Button("⏹️ Cancel", GUILayout.Height(30))) { isConnecting = false; SynLog.Info("[Synaptic] AI connection cancelled"); Repaint(); } EditorGUILayout.EndVertical(); } private async void StartAIConnection() { try { SynLog.Info("[Synaptic] Starting AI Connection..."); // Start animation isConnecting = true; animationTime = 0f; Repaint(); // Auto-setup if needed if (mcpSetupManager != null) { var status = await mcpSetupManager.CheckSetupStatus(); if (!status.isMCPInstalled) { await mcpSetupManager.RunCompleteSetup(); } } // Start MCP server mcpServerRunning = await mcpSetupManager.StartMCPServer(); // Stop animation isConnecting = false; if (mcpServerRunning) { // Auto-generate configuration files for desktop AI GenerateDesktopAIConfigs(); SynLog.Info("[Synaptic] ✅ AI connection setup complete! Desktop AI will auto-connect"); EditorUtility.DisplayDialog("AI Connection Ready", "Connection completed successfully.\n\n" + "Unity tools are now available in AI apps.\n" + "Type \"tools\" or \"unity\" to use the tools.", "OK"); } else { Debug.LogError("[Synaptic] ❌ Failed to start AI connection"); // Detailed guide when MCP server is not found EditorUtility.DisplayDialog( "MCP Server Not Found", "Please launch Claude Desktop first to start AI connection.\n\n" + "Steps:\n" + "1. Launch Claude Desktop app\n" + "2. Wait a moment, then press 'Start AI Connection' again\n\n" + "Note: Unity acts as a client to the MCP server.", "OK"); } Repaint(); } catch (Exception e) { Debug.LogError($"[Synaptic] AI connection error: {e.Message}"); isConnecting = false; // Stop animation mcpServerRunning = false; Repaint(); } } private void StopAIConnection() { mcpServerRunning = false; SynLog.Info("[Synaptic] AI Connection stopped"); Repaint(); } private void ShowUsageGuide() { EditorUtility.DisplayDialog( "How to Use Unity MCP Tools", "Tips for reliable tool usage\n\n" + "1. Launch your AI tool:\n" + " • Claude Desktop / Cursor / VS Code\n\n" + "2. Start a new chat\n\n" + "3. First, ask the AI:\n" + " • \"What tools are available?\"\n" + " • \"What can unity tools do?\"\n\n" + "4. Then, give specific instructions:\n" + " • \"Use unity tools to create a red cube\"\n" + " • \"Add a Player controller with tools\"\n\n" + "※ Including words like \"tools\" or \"unity\"\n" + " helps the AI use tools more reliably!", "OK" ); } private void ShowAIAppsDialog() { var option = EditorUtility.DisplayDialogComplex( "Select Desktop AI App", "Which AI app would you like to use?\n\n" + "ChatGPT: The world's most popular AI\n" + "Claude: AI with advanced code understanding\n\n" + "※ MCP-compatible version required", "ChatGPT", "Claude Desktop", "Cancel" ); switch (option) { case 0: // ChatGPT Application.OpenURL("https://chatgpt.com/"); break; case 1: // Claude Application.OpenURL("https://claude.ai/download"); break; } } // Helper method to get appropriate server path based on selected AI client private string GetServerScriptPath() { var mcpServerPath = FindMCPServerPath(); if (selectedAIClient == AIClientType.TokenSuperSaveMode) { // Token SuperSave mode: 3 meta-tools only return Path.Combine(mcpServerPath, "index-supersave.js"); } else if (selectedAIClient == AIClientType.GitHubCopilot) { // Dynamic mode for GitHub Copilot (VS Code) return Path.Combine(mcpServerPath, "hub-server.js"); } else if (selectedAIClient == AIClientType.CursorOrLMStudioEssential) { // Essential mode: 80 tools return Path.Combine(mcpServerPath, "index-essential.js"); } else { // Full mode: 246 tools return Path.Combine(mcpServerPath, "index.js"); } } // Update package.json "type" field based on selected server private void UpdatePackageJsonForSelectedServer() { try { var mcpServerPath = FindMCPServerPath(); var packageJsonPath = Path.Combine(mcpServerPath, "package.json"); if (!File.Exists(packageJsonPath)) { SynLog.Warn("[Synaptic] package.json not found. Skipping update."); return; } // Read existing package.json var packageJsonContent = File.ReadAllText(packageJsonPath); var packageJson = JsonConvert.DeserializeObject>(packageJsonContent); if (selectedAIClient == AIClientType.GitHubCopilot) { // hub-server.js uses ESM (import) - requires "type": "module" packageJson["type"] = "module"; packageJson["main"] = "hub-server.js"; SynLog.Info("[Synaptic] package.json updated for hub-server.js (ESM)"); } else if (selectedAIClient == AIClientType.TokenSuperSaveMode) { // index-supersave.js uses CommonJS (require) - remove "type" field if (packageJson.ContainsKey("type")) { packageJson.Remove("type"); } packageJson["main"] = "index-supersave.js"; SynLog.Info("[Synaptic] package.json updated for index-supersave.js (CommonJS)"); } else { // index.js uses CommonJS (require) - remove "type" field or set to "commonjs" if (packageJson.ContainsKey("type")) { packageJson.Remove("type"); } packageJson["main"] = "index.js"; SynLog.Info("[Synaptic] package.json updated for index.js (CommonJS)"); } // Write updated package.json File.WriteAllText(packageJsonPath, JsonConvert.SerializeObject(packageJson, Newtonsoft.Json.Formatting.Indented)); } catch (Exception e) { Debug.LogError($"[Synaptic] Failed to update package.json: {e.Message}"); } } private void GenerateDesktopAIConfigs() { try { var detectedAIs = DetectInstalledAIs(); var configuredCount = 0; var configuredTools = new List(); foreach (var ai in detectedAIs) { switch (ai.ToLower()) { case "claude": if (GenerateClaudeConfig()) { configuredCount++; configuredTools.Add("Claude Desktop"); } break; case "chatgpt": if (GenerateChatGPTConfig()) { configuredCount++; configuredTools.Add("ChatGPT Desktop"); } break; case "gemini": if (GenerateGeminiConfig()) { configuredCount++; configuredTools.Add("Gemini Desktop"); } break; } } // Always configure Cursor (user-level config) if (GenerateCursorConfig()) { configuredCount++; configuredTools.Add("Cursor"); } // Always configure VS Code (project-level config) if (GenerateVSCodeConfig()) { configuredCount++; configuredTools.Add("VS Code"); } // Always configure LM Studio (user-level config) if (GenerateLMStudioConfig()) { configuredCount++; configuredTools.Add("LM Studio"); } // Configure CLI tools (all in one setup) try { if (GenerateClaudeCodeSpecificConfig()) { configuredCount++; configuredTools.Add("Claude Code"); } } catch { /* Ignore individual CLI errors */ } try { GenerateWindsurfConfig(); configuredCount++; configuredTools.Add("Windsurf"); } catch { /* Ignore individual CLI errors */ } try { if (GenerateAntigravityConfig()) { configuredCount++; configuredTools.Add("Google Antigravity"); } } catch { /* Ignore individual CLI errors */ } try { if (GenerateKiroConfig()) { configuredCount++; configuredTools.Add("Amazon Kiro"); } } catch { /* Ignore individual CLI errors */ } // Update MCPServer/mcp-config.json with current project path UpdateMCPServerConfig(); SynLog.Info($"[Synaptic] Auto-generated {configuredCount} MCP configurations: {string.Join(", ", configuredTools)}"); } catch (Exception e) { Debug.LogError($"[Synaptic] Configuration file generation error: {e.Message}"); } } private List DetectInstalledAIs() { var installedAIs = new List(); if (Application.platform == RuntimePlatform.OSXEditor) { // macOS application detection var appsDir = "/Applications"; if (Directory.Exists($"{appsDir}/Claude.app")) installedAIs.Add("Claude"); if (Directory.Exists($"{appsDir}/ChatGPT.app")) installedAIs.Add("ChatGPT"); if (Directory.Exists($"{appsDir}/Gemini.app")) installedAIs.Add("Gemini"); // Homebrew cask installation detection var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); if (Directory.Exists($"{homeDir}/Applications/Claude.app")) installedAIs.Add("Claude"); if (Directory.Exists($"{homeDir}/Applications/ChatGPT.app")) installedAIs.Add("ChatGPT"); } else if (Application.platform == RuntimePlatform.WindowsEditor) { // Windows installation detection var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); // Claude: Check installation paths, config directory, AND Microsoft Store var hasClaude = Directory.Exists($"{programFiles}/Claude") || Directory.Exists($"{localAppData}/Programs/Claude") || Directory.Exists($"{appData}/Claude"); // Also check for Microsoft Store version if (!hasClaude) { var packagesDir = Path.Combine(localAppData, "Packages"); if (Directory.Exists(packagesDir)) { try { var claudePackages = Directory.GetDirectories(packagesDir, "Claude_*"); if (claudePackages.Length == 0) { // Fallback: enumerate and filter var allPackages = Directory.GetDirectories(packagesDir); hasClaude = allPackages.Any(p => Path.GetFileName(p).StartsWith("Claude", StringComparison.OrdinalIgnoreCase)); } else { hasClaude = true; } } catch { } } } if (hasClaude) { installedAIs.Add("Claude"); } // ChatGPT if (Directory.Exists($"{programFiles}/ChatGPT") || Directory.Exists($"{localAppData}/ChatGPT") || Directory.Exists($"{appData}/ChatGPT")) { installedAIs.Add("ChatGPT"); } } return installedAIs.Distinct().ToList(); } private bool GenerateClaudeConfig() { try { var claudeConfigDir = DetectClaudeConfigPath(); if (string.IsNullOrEmpty(claudeConfigDir)) { SynLog.Warn("[Synaptic] Claude Desktop config path not detected."); return false; } if (!Directory.Exists(claudeConfigDir)) { Directory.CreateDirectory(claudeConfigDir); } var configPath = Path.Combine(claudeConfigDir, "claude_desktop_config.json"); // Load existing configuration dynamic existingConfig = null; if (File.Exists(configPath)) { try { var existingJson = File.ReadAllText(configPath); existingConfig = JsonConvert.DeserializeObject(existingJson); } catch (Exception e) { SynLog.Warn($"[Synaptic] Failed to load existing Claude configuration: {e.Message}"); } } // Unity MCP server configuration (v1.1.0: uses selected server script) // Normalize paths for cross-platform JSON compatibility (Windows: \ -> /) var unityMcpServer = new { command = FindNodePath(), args = new[] { NormalizePathForJson(GetServerScriptPath()) }, cwd = NormalizePathForJson(FindMCPServerPath()), // CRITICAL: Node.js needs to run from MCPServer directory env = new { } }; // Merge with existing configuration dynamic claudeConfig; if (existingConfig?.mcpServers != null) { // Preserve existing mcpServers and add unity server var mcpServers = JsonConvert.DeserializeObject>( JsonConvert.SerializeObject(existingConfig.mcpServers)); mcpServers["unity-synaptic"] = unityMcpServer; claudeConfig = new { mcpServers = mcpServers }; // Preserve other existing settings var configDict = JsonConvert.DeserializeObject>( JsonConvert.SerializeObject(existingConfig)); configDict["mcpServers"] = mcpServers; claudeConfig = configDict; } else { // Create new configuration claudeConfig = new { mcpServers = new Dictionary { ["unity-synaptic"] = unityMcpServer } }; } File.WriteAllText(configPath, JsonConvert.SerializeObject(claudeConfig, Newtonsoft.Json.Formatting.Indented)); SynLog.Info($"[Synaptic] Claude configuration file created: {configPath}"); return true; } catch (Exception e) { Debug.LogError($"[Synaptic] Claude configuration error: {e.Message}"); return false; } } private string DetectClaudeConfigPath() { if (Application.platform == RuntimePlatform.OSXEditor) { // macOS Claude configuration path candidates var candidates = new[] { Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Claude"), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "com.anthropic.claudefordesktop"), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "claude") }; foreach (var path in candidates) { var parentDir = Path.GetDirectoryName(path); if (Directory.Exists(parentDir)) { return path; } } } else if (Application.platform == RuntimePlatform.WindowsEditor) { // Windows Claude configuration path candidates var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); // Find Microsoft Store package path dynamically string msStoreClaudePath = null; var packagesDir = Path.Combine(localAppData, "Packages"); if (Directory.Exists(packagesDir)) { try { // Method 1: Direct pattern match var claudePackages = Directory.GetDirectories(packagesDir, "Claude_*"); // Method 2: If pattern fails, enumerate and filter manually if (claudePackages.Length == 0) { var allPackages = Directory.GetDirectories(packagesDir); claudePackages = allPackages.Where(p => Path.GetFileName(p).StartsWith("Claude", StringComparison.OrdinalIgnoreCase) ).ToArray(); } if (claudePackages.Length > 0) { var candidatePath = Path.Combine(claudePackages[0], "LocalCache", "Roaming", "Claude"); // Verify the path actually exists or has config if (Directory.Exists(candidatePath) || File.Exists(Path.Combine(candidatePath, "claude_desktop_config.json"))) { msStoreClaudePath = candidatePath; } else { // Even if directory doesn't exist yet, use it if package is found msStoreClaudePath = candidatePath; } } } catch { /* Ignore directory access errors */ } } var candidatesList = new List { Path.Combine(appData, "Claude"), // %APPDATA%\Claude (most common) Path.Combine(localAppData, "Claude"), // %LOCALAPPDATA%\Claude Path.Combine(localAppData, "Programs", "Claude"), // %LOCALAPPDATA%\Programs\Claude }; // If Microsoft Store version is installed, ALWAYS use that path // (MS Store version only reads from its sandboxed location) if (!string.IsNullOrEmpty(msStoreClaudePath)) { return msStoreClaudePath; } var candidates = candidatesList.ToArray(); // For standard installation, check if existing config file exists foreach (var path in candidates) { var configFile = Path.Combine(path, "claude_desktop_config.json"); if (File.Exists(configFile)) { return path; } } // If no existing config, check if Claude directory exists foreach (var path in candidates) { if (Directory.Exists(path)) { return path; } } // Default to standard %APPDATA%\Claude path (will create if needed) return Path.Combine(appData, "Claude"); } return null; } private bool GenerateGeminiConfig() { try { var geminiConfigDir = DetectGeminiConfigPath(); if (string.IsNullOrEmpty(geminiConfigDir)) { SynLog.Warn("[Synaptic] Gemini Desktop not found."); return false; } if (!Directory.Exists(geminiConfigDir)) { Directory.CreateDirectory(geminiConfigDir); } var geminiConfig = new { servers = new[] { new { name = "unity-synaptic", url = "http://localhost:3000", type = "mcp" } } }; var configPath = Path.Combine(geminiConfigDir, "config.json"); File.WriteAllText(configPath, JsonConvert.SerializeObject(geminiConfig, Newtonsoft.Json.Formatting.Indented)); SynLog.Info($"[Synaptic] Gemini configuration file created: {configPath}"); return true; } catch (Exception e) { Debug.LogError($"[Synaptic] Gemini configuration error: {e.Message}"); return false; } } private string DetectGeminiConfigPath() { if (Application.platform == RuntimePlatform.OSXEditor) { var candidates = new[] { Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "gemini"), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Gemini") }; foreach (var path in candidates) { var parentDir = Path.GetDirectoryName(path); if (Directory.Exists(parentDir)) { return path; } } } else if (Application.platform == RuntimePlatform.WindowsEditor) { return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Gemini"); } return null; } private string DetectCursorConfigPath() { // Cursor native MCP config: ~/.cursor/mcp.json var cursorDir = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cursor" ); return cursorDir; } private bool GenerateCursorConfig() { try { var cursorConfigDir = DetectCursorConfigPath(); if (string.IsNullOrEmpty(cursorConfigDir)) { SynLog.Warn("[Synaptic] Could not determine Cursor config path."); return false; } if (!Directory.Exists(cursorConfigDir)) { Directory.CreateDirectory(cursorConfigDir); } var configPath = Path.Combine(cursorConfigDir, "mcp.json"); // Load existing configuration dynamic existingConfig = null; if (File.Exists(configPath)) { try { var existingJson = File.ReadAllText(configPath); existingConfig = JsonConvert.DeserializeObject(existingJson); } catch (Exception e) { SynLog.Warn($"[Synaptic] Failed to load existing Cursor configuration: {e.Message}"); } } // Unity MCP server configuration (v1.1.3: uses selected server script) // Normalize paths for cross-platform JSON compatibility (Windows: \ -> /) // Essential mode uses index-essential.js (80 tools), Full mode uses index.js (246 tools) var unityMcpServer = new { command = FindNodePath(), args = new[] { NormalizePathForJson(GetServerScriptPath()) }, cwd = NormalizePathForJson(FindMCPServerPath()) // CRITICAL: Node.js needs to run from MCPServer directory }; // Merge with existing configuration dynamic cursorConfig; if (existingConfig?.mcpServers != null) { var mcpServers = JsonConvert.DeserializeObject>( JsonConvert.SerializeObject(existingConfig.mcpServers)); mcpServers["unity-synaptic"] = unityMcpServer; var configDict = JsonConvert.DeserializeObject>( JsonConvert.SerializeObject(existingConfig)); configDict["mcpServers"] = mcpServers; cursorConfig = configDict; } else { cursorConfig = new { mcpServers = new Dictionary { ["unity-synaptic"] = unityMcpServer } }; } File.WriteAllText(configPath, JsonConvert.SerializeObject(cursorConfig, Newtonsoft.Json.Formatting.Indented)); SynLog.Info($"[Synaptic] ✅ Cursor configuration file created: {configPath}"); return true; } catch (Exception e) { Debug.LogError($"[Synaptic] Cursor configuration error: {e.Message}"); return false; } } private string DetectVSCodeConfigPath() { // VS Code project-level MCP config: .vscode/mcp.json var projectPath = Application.dataPath.Replace("/Assets", ""); var vscodeDir = Path.Combine(projectPath, ".vscode"); return vscodeDir; } private bool GenerateVSCodeConfig() { try { var vscodeConfigDir = DetectVSCodeConfigPath(); if (string.IsNullOrEmpty(vscodeConfigDir)) { SynLog.Warn("[Synaptic] Could not determine VS Code config path."); return false; } if (!Directory.Exists(vscodeConfigDir)) { Directory.CreateDirectory(vscodeConfigDir); } var configPath = Path.Combine(vscodeConfigDir, "mcp.json"); // Load existing configuration dynamic existingConfig = null; if (File.Exists(configPath)) { try { var existingJson = File.ReadAllText(configPath); existingConfig = JsonConvert.DeserializeObject(existingJson); } catch (Exception e) { SynLog.Warn($"[Synaptic] Failed to load existing VS Code configuration: {e.Message}"); } } // Unity MCP server configuration (v1.1.0: VS Code format with "servers" and "type") // Normalize paths for cross-platform JSON compatibility (Windows: \ -> /) var unityMcpServer = new { type = "stdio", // Required by VS Code command = FindNodePath(), args = new[] { NormalizePathForJson(GetServerScriptPath()) }, cwd = NormalizePathForJson(FindMCPServerPath()) // CRITICAL: Node.js needs to run from MCPServer directory }; // Merge with existing configuration (VS Code uses "servers", not "mcpServers") dynamic vscodeConfig; if (existingConfig?.servers != null) { var servers = JsonConvert.DeserializeObject>( JsonConvert.SerializeObject(existingConfig.servers)); servers["unity-synaptic"] = unityMcpServer; var configDict = JsonConvert.DeserializeObject>( JsonConvert.SerializeObject(existingConfig)); configDict["servers"] = servers; vscodeConfig = configDict; } else { vscodeConfig = new { servers = new Dictionary { ["unity-synaptic"] = unityMcpServer } }; } File.WriteAllText(configPath, JsonConvert.SerializeObject(vscodeConfig, Newtonsoft.Json.Formatting.Indented)); SynLog.Info($"[Synaptic] ✅ VS Code configuration file created: {configPath}"); return true; } catch (Exception e) { Debug.LogError($"[Synaptic] VS Code configuration error: {e.Message}"); return false; } } private string DetectLMStudioConfigPath() { var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); if (Application.platform == RuntimePlatform.OSXEditor) { // macOS: ~/.lmstudio/ return Path.Combine(homeDir, ".lmstudio"); } else if (Application.platform == RuntimePlatform.WindowsEditor) { // Windows: %USERPROFILE%/.lmstudio/ return Path.Combine(homeDir, ".lmstudio"); } return null; } private bool GenerateLMStudioConfig() { try { var lmstudioConfigDir = DetectLMStudioConfigPath(); if (string.IsNullOrEmpty(lmstudioConfigDir)) { SynLog.Warn("[Synaptic] Could not determine LM Studio config path."); return false; } if (!Directory.Exists(lmstudioConfigDir)) { Directory.CreateDirectory(lmstudioConfigDir); } var configPath = Path.Combine(lmstudioConfigDir, "mcp.json"); // Load existing configuration dynamic existingConfig = null; if (File.Exists(configPath)) { try { var existingJson = File.ReadAllText(configPath); existingConfig = JsonConvert.DeserializeObject(existingJson); } catch (Exception e) { SynLog.Warn($"[Synaptic] Failed to load existing LM Studio configuration: {e.Message}"); } } // Unity MCP server configuration (LM Studio uses same format as Cursor) // Normalize paths for cross-platform JSON compatibility (Windows: \ -> /) var unityMcpServer = new { command = FindNodePath(), args = new[] { NormalizePathForJson(GetServerScriptPath()) }, cwd = NormalizePathForJson(FindMCPServerPath()), // CRITICAL: Node.js needs to run from MCPServer directory env = new { } }; // Merge with existing configuration dynamic lmstudioConfig; if (existingConfig?.mcpServers != null) { var mcpServers = JsonConvert.DeserializeObject>( JsonConvert.SerializeObject(existingConfig.mcpServers)); mcpServers["unity-synaptic"] = unityMcpServer; var configDict = JsonConvert.DeserializeObject>( JsonConvert.SerializeObject(existingConfig)); configDict["mcpServers"] = mcpServers; lmstudioConfig = configDict; } else { lmstudioConfig = new { mcpServers = new Dictionary { ["unity-synaptic"] = unityMcpServer } }; } File.WriteAllText(configPath, JsonConvert.SerializeObject(lmstudioConfig, Newtonsoft.Json.Formatting.Indented)); SynLog.Info($"[Synaptic] ✅ LM Studio configuration file created: {configPath}"); return true; } catch (Exception e) { Debug.LogError($"[Synaptic] LM Studio configuration error: {e.Message}"); return false; } } private bool UpdateMCPServerConfig() { try { var mcpServerPath = FindMCPServerPath(); if (string.IsNullOrEmpty(mcpServerPath)) { SynLog.Warn("[Synaptic] MCP Server path not found."); return false; } var configPath = Path.Combine(mcpServerPath, "mcp-config.json"); if (!File.Exists(configPath)) { SynLog.Warn($"[Synaptic] mcp-config.json not found at: {configPath}"); return false; } // Read the config file var configContent = File.ReadAllText(configPath); // Replace placeholder with actual path configContent = configContent.Replace("{{PROJECT_MCP_SERVER_PATH}}", mcpServerPath); // Write back File.WriteAllText(configPath, configContent); SynLog.Info($"[Synaptic] ✅ Updated mcp-config.json with project path: {mcpServerPath}"); return true; } catch (Exception e) { Debug.LogError($"[Synaptic] mcp-config.json update error: {e.Message}"); return false; } } private bool GenerateChatGPTConfig() { try { var chatgptConfigDir = DetectChatGPTConfigPath(); if (string.IsNullOrEmpty(chatgptConfigDir)) { SynLog.Warn("[Synaptic] ChatGPT Desktop not found."); return false; } if (!Directory.Exists(chatgptConfigDir)) { Directory.CreateDirectory(chatgptConfigDir); } var configPath = Path.Combine(chatgptConfigDir, "config.json"); // Load existing configuration dynamic existingConfig = null; if (File.Exists(configPath)) { try { var existingJson = File.ReadAllText(configPath); existingConfig = JsonConvert.DeserializeObject(existingJson); } catch (Exception e) { SynLog.Warn($"[Synaptic] Failed to load existing ChatGPT configuration: {e.Message}"); } } // Unity MCP server configuration (v1.1.0: uses selected server script) // Normalize paths for cross-platform JSON compatibility (Windows: \ -> /) var unityMcpServer = new { command = FindNodePath(), args = new[] { NormalizePathForJson(GetServerScriptPath()) }, env = new { } }; // Merge with existing configuration dynamic chatgptConfig; if (existingConfig?.mcpServers != null) { // Preserve existing mcpServers and add unity server var mcpServers = JsonConvert.DeserializeObject>( JsonConvert.SerializeObject(existingConfig.mcpServers)); mcpServers["unity-synaptic"] = unityMcpServer; // Preserve other existing settings var configDict = JsonConvert.DeserializeObject>( JsonConvert.SerializeObject(existingConfig)); configDict["mcpServers"] = mcpServers; chatgptConfig = configDict; } else { // Create new configuration chatgptConfig = new { mcpServers = new Dictionary { ["unity-synaptic"] = unityMcpServer } }; } File.WriteAllText(configPath, JsonConvert.SerializeObject(chatgptConfig, Newtonsoft.Json.Formatting.Indented)); SynLog.Info($"[Synaptic] ChatGPT configuration file created: {configPath}"); return true; } catch (Exception e) { Debug.LogError($"[Synaptic] ChatGPT configuration error: {e.Message}"); return false; } } private string DetectChatGPTConfigPath() { if (Application.platform == RuntimePlatform.OSXEditor) { var candidates = new[] { Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "com.openai.chat"), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "ChatGPT"), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "chatgpt") }; foreach (var path in candidates) { var parentDir = Path.GetDirectoryName(path); if (Directory.Exists(parentDir)) { return path; } } } else if (Application.platform == RuntimePlatform.WindowsEditor) { return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ChatGPT"); } return null; } private async Task RefreshStatus() { mcpStatus = await mcpSetupManager.CheckSetupStatus(); Repaint(); } private void CheckMCPStatus() { // バックグラウンドスレッドで実行(Hold onを防ぐ) System.Threading.ThreadPool.QueueUserWorkItem(async _ => { try { mcpStatus = await mcpSetupManager.CheckSetupStatus(); EditorApplication.delayCall += () => { if (this) Repaint(); }; } catch { } }); } private void SaveMCPSettings() { // Save to .env file var envPath = Path.Combine(Application.dataPath.Replace("/Assets", ""), "MCPServer", ".env"); var envContent = new List { $"PORT={mcpPort}", $"WS_PORT={wsPort}" }; System.IO.File.WriteAllLines(envPath, envContent); SynLog.Info("[Synaptic] MCP settings saved"); } private async void StartMCPServer() { mcpServerRunning = await mcpSetupManager.StartMCPServer(); Repaint(); } private void StopMCPServer() { // Server stop implementation mcpServerRunning = false; Repaint(); } private async void RestartMCPServer() { StopMCPServer(); await Task.Delay(1000); await Task.Run(() => StartMCPServer()); } private void ConfigureMCP() { try { SynLog.Info("[Synaptic] Starting MCP setup..."); // Check and install dependencies if (!CheckAndInstallDependencies()) { EditorUtility.DisplayDialog( "Setup Error", "Failed to install required dependencies. Please check the console for details.", "OK" ); return; } // Generate MCP configuration files GenerateDesktopAIConfigs(); // Update package.json based on selected server UpdatePackageJsonForSelectedServer(); // Initialize MCP server if (mcpSetupManager == null) { mcpSetupManager = NexusMCPSetupManager.Instance; } // Set configuration complete flag mcpConfigured = true; SynLog.Info("[Synaptic] MCP setup completed. Ready for AI integration."); // Display success message based on selected AI client (v1.1.0) string successMessage; if (selectedAIClient == AIClientType.GitHubCopilot) { successMessage = "MCP configuration completed for GitHub Copilot!\n\n" + "Mode: Dynamic Tool Loading (hub-server.js)\n" + "Configuration: .vscode/mcp.json\n\n" + "⚠️ Important: Restart VS Code to activate Unity MCP.\n\n" + "Initial tools: 8 management tools\n" + "Dynamic loading: Use select_tools() to load more\n\n" + "Example:\n" + "\"Use select_tools to load GameObject and Material tools\""; } else { successMessage = "MCP configuration completed successfully!\n\n" + "Mode: Full Mode (index.js) - All 246 tools\n" + "Configurations created for:\n" + "• Claude Desktop (claude_desktop_config.json)\n" + "• Cursor (~/.cursor/mcp.json)\n" + "• VS Code (.vscode/mcp.json)\n\n" + "⚠️ Important: Restart/Reload your AI tool to activate Unity MCP.\n\n" + "After restarting, ask:\n" + "\"What Unity tools are available?\""; } EditorUtility.DisplayDialog( "MCP Setup Complete", successMessage, "OK" ); Repaint(); } catch (Exception e) { Debug.LogError($"[Synaptic] MCP configuration error: {e.Message}"); EditorUtility.DisplayDialog( "MCP Setup Error", $"An error occurred during MCP setup:\n{e.Message}", "OK" ); } } private void ResetMCPConfiguration() { var confirmed = EditorUtility.DisplayDialog( "Reset MCP Settings", "Reset MCP settings and reconfigure?\n\n" + "Current AI connection will also be stopped.", "Reset", "Cancel" ); if (confirmed) { // Reset configuration flag mcpConfigured = false; mcpSetupManager = null; SynLog.Info("[Synaptic] MCP settings have been reset. Please reconfigure."); EditorUtility.DisplayDialog( "Settings Reset Complete", "MCP settings have been reset.\n" + "Please reconfigure from the 'Complete MCP Setup' button.", "OK" ); Repaint(); } } // CLI AI configuration generation methods private void GenerateClaudeCodeConfig() { try { // Create Claude Code specific configuration (no detection required) if (GenerateClaudeCodeSpecificConfig()) { SynLog.Info("[Synaptic] Claude Code configuration complete"); EditorUtility.DisplayDialog( "Claude Code Configuration Complete", "Claude Code configuration has been completed.\n\n" + "Configuration files:\n" + "• Project: .claude/settings.local.json\n" + "• Global: ~/.claude.json\n\n" + "How to use:\n" + "1. Restart Claude Code\n" + "2. Unity MCP tools will be automatically enabled\n\n" + "Verification commands:\n" + "• claude mcp list\n" + "• claude mcp get unity-synaptic", "OK" ); } else { EditorUtility.DisplayDialog("Error", "Failed to create Claude Code configuration. Please check Unity Console for errors.", "OK"); } } catch (Exception e) { Debug.LogError($"[Synaptic] Claude Code configuration error: {e.Message}"); EditorUtility.DisplayDialog("Error", $"Claude Code configuration failed:\n{e.Message}", "OK"); } } // DEPRECATED: CLI MCP configurations removed - HTTP API is the recommended approach // private void GenerateGeminiCLIConfig() { ... } // private void GenerateCodexCLIConfig() { ... } private void GenerateWindsurfConfig() { try { // Windsurf uses user-level config var windsurfConfigPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".windsurf", "mcp_servers.json" ); var windsurfDir = Path.GetDirectoryName(windsurfConfigPath); if (!Directory.Exists(windsurfDir)) { Directory.CreateDirectory(windsurfDir); } // Unity MCP server settings // Normalize paths for cross-platform JSON compatibility (Windows: \ -> /) var unityMcpServer = new { command = FindNodePath(), args = new[] { NormalizePathForJson(GetServerScriptPath()) }, env = new Dictionary(), description = "Unity game development tools" }; // Load existing settings dynamic existingConfig = null; if (File.Exists(windsurfConfigPath)) { try { var existingJson = File.ReadAllText(windsurfConfigPath); existingConfig = JsonConvert.DeserializeObject(existingJson); } catch (Exception e) { SynLog.Warn($"[Synaptic] Failed to load existing Windsurf config: {e.Message}"); } } // Windsurf MCP settings (2025 specification) dynamic windsurfConfig; if (existingConfig?.servers != null) { var servers = JsonConvert.DeserializeObject>( JsonConvert.SerializeObject(existingConfig.servers)); servers["unity-synaptic"] = unityMcpServer; var configDict = JsonConvert.DeserializeObject>( JsonConvert.SerializeObject(existingConfig)); configDict["servers"] = servers; windsurfConfig = configDict; } else { windsurfConfig = new { servers = new Dictionary { ["unity-synaptic"] = unityMcpServer } }; } File.WriteAllText(windsurfConfigPath, JsonConvert.SerializeObject(windsurfConfig, Newtonsoft.Json.Formatting.Indented)); SynLog.Info($"[Synaptic] Windsurf config file created: {windsurfConfigPath}"); EditorUtility.DisplayDialog( "Windsurf Configuration Complete", "Windsurf MCP configuration created successfully.\n\n" + "Config file: ~/.windsurf/mcp_servers.json\n\n" + "Next steps:\n" + "1. Restart Windsurf\n" + "2. Unity MCP server will be available\n" + "3. Use '@unity-synaptic' to access Unity tools\n\n" + "The IDE that writes with you!", "OK" ); } catch (Exception e) { Debug.LogError($"[Synaptic] Windsurf config error: {e.Message}"); EditorUtility.DisplayDialog("Error", $"Failed to create Windsurf configuration:\n{e.Message}", "OK"); } } private bool GenerateAntigravityConfig() { try { // Google Antigravity config path string configPath; if (Application.platform == RuntimePlatform.OSXEditor) { // macOS: ~/.gemini/antigravity/mcp_config.json configPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "antigravity", "mcp_config.json" ); } else if (Application.platform == RuntimePlatform.WindowsEditor) { // Windows: %USERPROFILE%\.gemini\antigravity\mcp_config.json configPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "antigravity", "mcp_config.json" ); } else { SynLog.Warn("[Synaptic] Antigravity config: Unsupported platform"); return false; } var configDir = Path.GetDirectoryName(configPath); if (!Directory.Exists(configDir)) { Directory.CreateDirectory(configDir); } // Load existing configuration dynamic existingConfig = null; if (File.Exists(configPath)) { try { var existingJson = File.ReadAllText(configPath); existingConfig = JsonConvert.DeserializeObject(existingJson); } catch (Exception e) { SynLog.Warn($"[Synaptic] Failed to load existing Antigravity config: {e.Message}"); } } // Unity MCP server configuration var unityMcpServer = new { command = FindNodePath(), args = new[] { NormalizePathForJson(GetServerScriptPath()) } }; // Merge with existing configuration dynamic antigravityConfig; if (existingConfig?.mcpServers != null) { var mcpServers = JsonConvert.DeserializeObject>( JsonConvert.SerializeObject(existingConfig.mcpServers)); mcpServers["unity-synaptic"] = unityMcpServer; var configDict = JsonConvert.DeserializeObject>( JsonConvert.SerializeObject(existingConfig)); configDict["mcpServers"] = mcpServers; antigravityConfig = configDict; } else { antigravityConfig = new { mcpServers = new Dictionary { ["unity-synaptic"] = unityMcpServer } }; } File.WriteAllText(configPath, JsonConvert.SerializeObject(antigravityConfig, Newtonsoft.Json.Formatting.Indented)); SynLog.Info($"[Synaptic] ✅ Google Antigravity config created: {configPath}"); return true; } catch (Exception e) { Debug.LogError($"[Synaptic] Antigravity config error: {e.Message}"); return false; } } private bool GenerateKiroConfig() { try { // Amazon Kiro config path string configPath; if (Application.platform == RuntimePlatform.OSXEditor) { // macOS: ~/.kiro/settings/mcp.json configPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kiro", "settings", "mcp.json" ); } else if (Application.platform == RuntimePlatform.WindowsEditor) { // Windows: %USERPROFILE%\.kiro\settings\mcp.json configPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kiro", "settings", "mcp.json" ); } else { SynLog.Warn("[Synaptic] Kiro config: Unsupported platform"); return false; } var configDir = Path.GetDirectoryName(configPath); if (!Directory.Exists(configDir)) { Directory.CreateDirectory(configDir); } // Load existing configuration dynamic existingConfig = null; if (File.Exists(configPath)) { try { var existingJson = File.ReadAllText(configPath); existingConfig = JsonConvert.DeserializeObject(existingJson); } catch (Exception e) { SynLog.Warn($"[Synaptic] Failed to load existing Kiro config: {e.Message}"); } } // Unity MCP server configuration var unityMcpServer = new { command = FindNodePath(), args = new[] { NormalizePathForJson(GetServerScriptPath()) }, disabled = false }; // Merge with existing configuration dynamic kiroConfig; if (existingConfig?.mcpServers != null) { var mcpServers = JsonConvert.DeserializeObject>( JsonConvert.SerializeObject(existingConfig.mcpServers)); mcpServers["unity-synaptic"] = unityMcpServer; var configDict = JsonConvert.DeserializeObject>( JsonConvert.SerializeObject(existingConfig)); configDict["mcpServers"] = mcpServers; kiroConfig = configDict; } else { kiroConfig = new { mcpServers = new Dictionary { ["unity-synaptic"] = unityMcpServer } }; } File.WriteAllText(configPath, JsonConvert.SerializeObject(kiroConfig, Newtonsoft.Json.Formatting.Indented)); SynLog.Info($"[Synaptic] ✅ Amazon Kiro config created: {configPath}"); return true; } catch (Exception e) { Debug.LogError($"[Synaptic] Kiro config error: {e.Message}"); return false; } } private void GenerateAllCLIConfigs() { try { SynLog.Info("[Synaptic] Configuring all MCP clients..."); var configuredTools = new List(); // Claude Code try { if (GenerateClaudeCodeSpecificConfig()) { configuredTools.Add("Claude Code"); } } catch { /* Ignore individual CLI errors */ } // Cursor try { if (GenerateCursorConfig()) { configuredTools.Add("Cursor"); } } catch (Exception e) { SynLog.Warn($"[Synaptic] Cursor config skipped: {e.Message}"); } // VS Code try { if (GenerateVSCodeConfig()) { configuredTools.Add("VS Code"); } } catch (Exception e) { SynLog.Warn($"[Synaptic] VS Code config skipped: {e.Message}"); } // Windsurf try { GenerateWindsurfConfig(); configuredTools.Add("Windsurf"); } catch (Exception e) { SynLog.Warn($"[Synaptic] Windsurf config skipped: {e.Message}"); } // Google Antigravity try { if (GenerateAntigravityConfig()) { configuredTools.Add("Google Antigravity"); } } catch (Exception e) { SynLog.Warn($"[Synaptic] Antigravity config skipped: {e.Message}"); } // Amazon Kiro try { if (GenerateKiroConfig()) { configuredTools.Add("Amazon Kiro"); } } catch (Exception e) { SynLog.Warn($"[Synaptic] Kiro config skipped: {e.Message}"); } // Generic settings for other MCP-compatible tools GenerateUniversalCLIConfig(); configuredTools.Add("Generic MCP Clients"); SynLog.Info($"[Synaptic] All CLI AI configs completed: {string.Join(", ", configuredTools)}"); EditorUtility.DisplayDialog( "MCP Client Configuration Complete", $"Successfully configured the following MCP clients:\n\n" + $"✅ {string.Join("\n✅ ", configuredTools)}\n\n" + "Unity MCP tools are now available in these applications.\n\n" + "Next steps:\n" + "1. Restart the configured applications\n" + "2. Unity MCP tools will be automatically available\n" + "3. Check each app's MCP panel or use '@unity-synaptic' mention", "OK" ); } catch (Exception e) { Debug.LogError($"[Synaptic] All CLI AI config error: {e.Message}"); EditorUtility.DisplayDialog("Error", $"Error during CLI AI configuration:\n{e.Message}", "OK"); } } private void GenerateUniversalCLIConfig() { try { var homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); var configDir = Path.Combine(homeDir, ".config", "synaptic-pro"); if (!Directory.Exists(configDir)) { Directory.CreateDirectory(configDir); } // Generic MCP config file // Normalize paths for cross-platform JSON compatibility (Windows: \ -> /) var universalConfig = new { mcp_servers = new Dictionary { ["unity-synaptic"] = new { command = FindNodePath(), args = new[] { NormalizePathForJson(Path.Combine(FindMCPServerPath(), "index.js")) }, env = new { }, description = "Unity game development tools via MCP", capabilities = new[] { "game_objects", "ui_creation", "scripting", "ai_agents", "animation" } } }, version = NexusVersion.Current, created_by = "Synaptic Pro Unity Integration" }; var configPath = Path.Combine(configDir, "mcp-config.json"); File.WriteAllText(configPath, JsonConvert.SerializeObject(universalConfig, Newtonsoft.Json.Formatting.Indented)); // Create usage example script var scriptPath = Path.Combine(configDir, "start-unity-mcp.sh"); var scriptContent = $@"#!/bin/bash # Unity MCP Server Starter Script # Generated by Synaptic Pro Unity Integration export MCP_CONFIG_PATH=""{NormalizePathForJson(configPath)}"" export UNITY_PROJECT_PATH=""{NormalizePathForJson(Application.dataPath.Replace($"{Path.DirectorySeparatorChar}Assets", ""))}"" export MCP_SERVER_PATH=""{NormalizePathForJson(FindMCPServerPath())}"" echo ""Starting Unity MCP Server..."" echo ""Project: $UNITY_PROJECT_PATH"" echo ""MCP Server: $MCP_SERVER_PATH"" echo ""Config: $MCP_CONFIG_PATH"" node ""$MCP_SERVER_PATH/index.js"" "; File.WriteAllText(scriptPath, scriptContent); // Grant execute permission (Unix) if (Application.platform == RuntimePlatform.OSXEditor) { System.Diagnostics.Process.Start("chmod", $"+x \"{scriptPath}\""); } SynLog.Info($"[Synaptic] Generic CLI config created: {configPath}"); } catch (Exception e) { SynLog.Warn($"[Synaptic] Failed to create generic CLI config: {e.Message}"); } } private void GenerateWatchConfig() { try { var projectPath = Application.dataPath.Replace("/Assets", ""); var watchConfigPath = Path.Combine(projectPath, ".synaptic-watch"); var watchConfig = new { watch_patterns = new[] { "**/*.cs", "**/*.js", "**/*.ts", "**/*.json", "**/*.md", "**/*.yaml", "**/*.yml" }, ignore_patterns = new[] { "**/node_modules/**", "**/Library/**", "**/Temp/**", "**/Logs/**", "**/obj/**", "**/bin/**" }, commands = new { on_change = new[] { "echo \"File changed: {file}\"", "# Add your custom commands here" } }, mcp_integration = new { enabled = true, server_url = "ws://127.0.0.1:8090", notify_on_change = true } }; File.WriteAllText(watchConfigPath, JsonConvert.SerializeObject(watchConfig, Newtonsoft.Json.Formatting.Indented)); // Create watch script var watchScriptPath = Path.Combine(projectPath, "watch-synaptic.sh"); var watchScript = $@"#!/bin/bash # Synaptic File Watcher Script # Monitors project files and notifies AI tools if command -v fswatch >/dev/null 2>&1; then echo ""Starting Synaptic file watcher with fswatch..."" fswatch -o . | while read f; do echo ""Files changed - notifying AI tools..."" # Add notification logic here done elif command -v inotifywait >/dev/null 2>&1; then echo ""Starting Synaptic file watcher with inotifywait..."" inotifywait -m -r -e modify,create,delete . | while read path action file; do echo ""$path$file $action - notifying AI tools..."" # Add notification logic here done else echo ""Please install fswatch (macOS) or inotify-tools (Linux)"" echo ""macOS: brew install fswatch"" echo ""Linux: sudo apt-get install inotify-tools"" fi "; File.WriteAllText(watchScriptPath, watchScript); // Grant execute permission if (Application.platform == RuntimePlatform.OSXEditor) { System.Diagnostics.Process.Start("chmod", $"+x \"{watchScriptPath}\""); } SynLog.Info($"[Synaptic] Watch config creation completed: {watchConfigPath}"); EditorUtility.DisplayDialog( "Watch Config Created", $"File monitoring configuration created:\n\n" + $"Config file: .synaptic-watch\n" + $"Script: watch-synaptic.sh\n\n" + "Monitored:\n" + "• C#, JavaScript, TypeScript\n" + "• JSON, Markdown, YAML\n\n" + "Usage: ./watch-synaptic.sh", "OK" ); } catch (Exception e) { Debug.LogError($"[Synaptic] Watch config error: {e.Message}"); EditorUtility.DisplayDialog("Error", $"Failed to create Watch config:\n{e.Message}", "OK"); } } // CLI detection and config path detection methods private string CheckCLIInstallation(string cliName) { try { // Try normal which command var startInfo = new System.Diagnostics.ProcessStartInfo { FileName = "which", Arguments = cliName, UseShellExecute = false, RedirectStandardOutput = true, CreateNoWindow = true }; using (var process = System.Diagnostics.Process.Start(startInfo)) { var output = process.StandardOutput.ReadToEnd().Trim(); process.WaitForExit(); if (process.ExitCode == 0 && !string.IsNullOrEmpty(output)) { return output; } } // CLI-specific path detection return CheckCLISpecificPaths(cliName); } catch (Exception e) { SynLog.Warn($"[Synaptic] CLI check error ({cliName}): {e.Message}"); } return null; } private string CheckCLISpecificPaths(string cliName) { switch (cliName) { case "claude-code": return CheckClaudeCodePaths(); case "gemini": return CheckGeminiCLIPaths(); case "codex": return CheckCodexCLIPaths(); default: return null; } } private string CheckClaudeCodePaths() { var paths = new[] { // Homebrew (Apple Silicon) "/opt/homebrew/Caskroom/claude-code/*/claude", "/opt/homebrew/bin/claude-code", // Homebrew (Intel) "/usr/local/Caskroom/claude-code/*/claude", "/usr/local/bin/claude-code", // NPM global "/usr/local/lib/node_modules/@anthropic/claude-code/bin/claude-code", Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/.npm-global/bin/claude-code", // Standalone installers "/Applications/Claude Code.app/Contents/MacOS/Claude Code", "/Applications/Claude.app/Contents/MacOS/Claude", // Direct download installs Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/Applications/Claude Code.app/Contents/MacOS/Claude Code", Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/Downloads/claude-code", Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/bin/claude-code", // pnpm/yarn global Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/.local/share/pnpm/claude-code", Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/.yarn/bin/claude-code" }; return FindExecutableInPaths(paths, "claude"); } private string CheckGeminiCLIPaths() { var paths = new[] { // Google Cloud SDK "/usr/local/bin/gcloud", "/opt/homebrew/bin/gcloud", Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/google-cloud-sdk/bin/gcloud", // Gemini specific CLI "/usr/local/bin/gemini", "/opt/homebrew/bin/gemini", // NPM installs "/usr/local/lib/node_modules/@google/gemini-cli/bin/gemini", Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/.npm-global/bin/gemini", // pip installs Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/.local/bin/gemini", Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/.local/bin/google-generativeai", // conda Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/anaconda3/bin/gemini", Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/miniconda3/bin/gemini", // Direct installs Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/bin/gemini", Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/Downloads/gemini", // Snap (if on Linux) "/snap/bin/gemini", // pnpm/yarn Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/.local/share/pnpm/gemini", Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/.yarn/bin/gemini" }; return FindExecutableInPaths(paths, "gemini"); } private string CheckCodexCLIPaths() { var paths = new[] { // NPM global installs "/usr/local/bin/codex", "/opt/homebrew/bin/codex", Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/.npm-global/bin/codex", // OpenAI official CLI "/usr/local/lib/node_modules/@openai/codex/bin/codex", Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/.npm-global/lib/node_modules/@openai/codex/bin/codex", // pnpm/yarn global Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/.local/share/pnpm/codex", Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/.yarn/bin/codex", // Direct installs Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/bin/codex", Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/Downloads/codex", // Homebrew "/opt/homebrew/bin/openai-codex", "/usr/local/bin/openai-codex", // Rust cargo install Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + "/.cargo/bin/codex", // Alternative names "/usr/local/bin/openai-codex-cli", "/opt/homebrew/bin/openai-codex-cli" }; return FindExecutableInPaths(paths, "codex"); } private string FindExecutableInPaths(string[] paths, string fallbackName) { foreach (var path in paths) { try { if (path.Contains("*")) { // Path with wildcards var baseDir = Path.GetDirectoryName(path); var fileName = Path.GetFileName(path); if (Directory.Exists(baseDir)) { var subdirs = Directory.GetDirectories(baseDir); foreach (var subdir in subdirs) { var fullPath = Path.Combine(subdir, fileName); if (File.Exists(fullPath)) { return fullPath; } } } } else if (File.Exists(path)) { return path; } } catch (Exception e) { SynLog.Warn($"[Synaptic] Path check error ({path}): {e.Message}"); } } // Detect from running processes return FindInRunningProcesses(fallbackName); } private string FindInRunningProcesses(string processName) { try { var psStartInfo = new System.Diagnostics.ProcessStartInfo { FileName = "ps", Arguments = "aux", UseShellExecute = false, RedirectStandardOutput = true, CreateNoWindow = true }; using (var psProcess = System.Diagnostics.Process.Start(psStartInfo)) { var psOutput = psProcess.StandardOutput.ReadToEnd(); psProcess.WaitForExit(); var lines = psOutput.Split('\n'); foreach (var line in lines) { // More strict process name matching if (IsProcessLineMatch(line, processName)) { var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries); if (parts.Length > 10) { var executablePath = parts[10]; if (File.Exists(executablePath) && IsCorrectExecutable(executablePath, processName)) { return executablePath; } } } } } } catch (Exception e) { SynLog.Warn($"[Synaptic] Process detection error ({processName}): {e.Message}"); } return null; } private bool IsProcessLineMatch(string line, string processName) { switch (processName.ToLower()) { case "claude": // For Claude Code: claude-code or claude, but exclude gemini etc return (line.Contains("claude-code") || line.Contains("/claude")) && !line.Contains("gemini") && !line.Contains("codex"); case "gemini": // For Gemini return line.Contains("gemini") && !line.Contains("claude") && !line.Contains("codex"); case "codex": // For Codex return line.Contains("codex") && !line.Contains("claude") && !line.Contains("gemini"); default: return line.Contains(processName); } } private bool IsCorrectExecutable(string executablePath, string processName) { var fileName = Path.GetFileNameWithoutExtension(executablePath).ToLower(); var fullPath = executablePath.ToLower(); switch (processName.ToLower()) { case "claude": // For Claude Code - distinguish from Claude Desktop if (fullPath.Contains("claude-code") || fullPath.Contains("caskroom/claude-code")) { return true; // Clearly Claude Code path } // Exclude Claude Desktop if (fullPath.Contains("/applications/claude.app") || fullPath.Contains("claude.app/contents")) { return false; // Exclude because it's Claude Desktop } return (fileName == "claude-code") && !fullPath.Contains("gemini") && !fullPath.Contains("codex"); case "gemini": return (fileName.Contains("gemini") || fileName == "gemini" || fileName == "gcloud") && (fullPath.Contains("gemini") || fullPath.Contains("google")) && !fullPath.Contains("claude") && !fullPath.Contains("codex"); case "codex": return (fileName.Contains("codex") || fileName == "codex" || fileName == "openai-codex") && fullPath.Contains("codex") && !fullPath.Contains("claude") && !fullPath.Contains("chatgpt") && !fullPath.Contains("gemini"); default: return fileName == processName.ToLower() || fileName.Contains(processName.ToLower()); } } private string DetectGeminiCLIConfigPath() { try { var candidates = new[] { Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "gemini", "config.json"), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "config.json"), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".google", "gemini", "config.json") }; foreach (var path in candidates) { var dir = Path.GetDirectoryName(path); if (Directory.Exists(dir) || path.Contains("gemini")) { return path; } } } catch (Exception e) { SynLog.Warn($"[Synaptic] Gemini CLI config path detection error: {e.Message}"); } return null; } private string DetectCodexCLIConfigPath() { try { var candidates = new[] { Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "openai", "codex.json"), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "codex", "config.json"), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codex", "config.json"), Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".openai", "codex.json") }; foreach (var path in candidates) { var dir = Path.GetDirectoryName(path); if (Directory.Exists(dir) || path.Contains("openai") || path.Contains("codex")) { return path; } } } catch (Exception e) { SynLog.Warn($"[Synaptic] OpenAI Codex CLI config path detection error: {e.Message}"); } return null; } private bool GenerateClaudeCodeSpecificConfig() { try { var projectPath = Application.dataPath.Replace("/Assets", ""); var claudeDir = Path.Combine(projectPath, ".claude"); var claudeConfigPath = Path.Combine(claudeDir, "settings.local.json"); // Load existing settings dynamic existingConfig = null; if (File.Exists(claudeConfigPath)) { try { var existingJson = File.ReadAllText(claudeConfigPath); existingConfig = JsonConvert.DeserializeObject(existingJson); } catch (Exception e) { SynLog.Warn($"[Synaptic] Failed to load existing Claude Code config: {e.Message}"); } } // Unity MCP server settings (2025 specification) // Normalize paths for cross-platform JSON compatibility (Windows: \ -> /) var unityMcpServer = new { command = FindNodePath(), args = new[] { NormalizePathForJson(GetServerScriptPath()) }, env = new Dictionary() }; // Create .claude directory if (!Directory.Exists(claudeDir)) { Directory.CreateDirectory(claudeDir); } // Project-specific config structure (2025 Claude Code format) dynamic claudeCodeConfig; if (existingConfig?.mcpServers != null) { // Get existing mcpServers var mcpServers = JsonConvert.DeserializeObject>( JsonConvert.SerializeObject(existingConfig.mcpServers)); mcpServers["unity-synaptic"] = unityMcpServer; // Update entire config var configDict = JsonConvert.DeserializeObject>( JsonConvert.SerializeObject(existingConfig)); configDict["mcpServers"] = mcpServers; claudeCodeConfig = configDict; } else { // Create new - 2025 MCP specification compliant claudeCodeConfig = new { mcpServers = new Dictionary { ["unity-synaptic"] = unityMcpServer }, permissions = new { allow = new[] { "mcp__unity-synaptic" }, deny = new object[] { } } }; } File.WriteAllText(claudeConfigPath, JsonConvert.SerializeObject(claudeCodeConfig, Newtonsoft.Json.Formatting.Indented)); SynLog.Info($"[Synaptic] Claude Code config file created: {claudeConfigPath}"); // Also create alternate config path (user global settings) var userConfigPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".claude.json" ); CreateUserLevelConfig(userConfigPath, unityMcpServer); return true; } catch (Exception e) { Debug.LogError($"[Synaptic] Claude Code specific config error: {e.Message}"); return false; } } private void CreateUserLevelConfig(string userConfigPath, object unityMcpServer) { try { dynamic userConfig = null; // Load existing user-level settings if (File.Exists(userConfigPath)) { try { var existingJson = File.ReadAllText(userConfigPath); userConfig = JsonConvert.DeserializeObject(existingJson); } catch (Exception e) { SynLog.Warn($"[Synaptic] Failed to load existing user settings: {e.Message}"); } } // Update or create user-level settings if (userConfig?.mcpServers != null) { var mcpServers = JsonConvert.DeserializeObject>( JsonConvert.SerializeObject(userConfig.mcpServers)); mcpServers["unity-synaptic"] = unityMcpServer; var configDict = JsonConvert.DeserializeObject>( JsonConvert.SerializeObject(userConfig)); configDict["mcpServers"] = mcpServers; userConfig = configDict; } else { userConfig = new { mcpServers = new Dictionary { ["unity-synaptic"] = unityMcpServer } }; } File.WriteAllText(userConfigPath, JsonConvert.SerializeObject(userConfig, Newtonsoft.Json.Formatting.Indented)); SynLog.Info($"[Synaptic] User global settings also created: {userConfigPath}"); } catch (Exception e) { SynLog.Warn($"[Synaptic] User global settings creation skipped: {e.Message}"); } } /// /// Normalize file path for JSON/Node.js consumption. /// Converts Windows backslashes to forward slashes. /// Node.js handles both, but forward slashes work cross-platform in JSON. /// private string NormalizePathForJson(string path) { if (string.IsNullOrEmpty(path)) return path; // Convert backslashes to forward slashes for cross-platform JSON compatibility return path.Replace("\\", "/"); } private static string _cachedMCPServerPath = null; private string FindMCPServerPath() { // キャッシュがあればそのまま返す(毎回の再帰検索を防ぐ) if (!string.IsNullOrEmpty(_cachedMCPServerPath) && Directory.Exists(_cachedMCPServerPath)) { return _cachedMCPServerPath; } // 1. まず既知のパスを直接チェック(高速) var knownPath = Path.Combine(Application.dataPath, "Synaptic AI Pro", "MCPServer"); if (Directory.Exists(knownPath)) { SynLog.Info($"[Synaptic] Found MCPServer at known path: {knownPath}"); _cachedMCPServerPath = knownPath; return knownPath; } // 2. Assets直下のみ検索(AllDirectoriesではなくTopDirectoryOnly + 1階層) try { foreach (var dir in Directory.GetDirectories(Application.dataPath, "*", SearchOption.TopDirectoryOnly)) { var mcpPath = Path.Combine(dir, "MCPServer"); if (Directory.Exists(mcpPath)) { SynLog.Info($"[Synaptic] Found MCPServer in Assets: {mcpPath}"); _cachedMCPServerPath = mcpPath; return mcpPath; } } } catch { } // 3. Packages(パッケージマネージャー経由) string projectPath = Application.dataPath.Replace($"{Path.DirectorySeparatorChar}Assets", ""); string packagesPath = Path.Combine(projectPath, "Packages"); if (Directory.Exists(packagesPath)) { try { foreach (var dir in Directory.GetDirectories(packagesPath, "*", SearchOption.TopDirectoryOnly)) { var mcpPath = Path.Combine(dir, "MCPServer"); if (Directory.Exists(mcpPath)) { SynLog.Info($"[Synaptic] Found MCPServer in Packages: {mcpPath}"); _cachedMCPServerPath = mcpPath; return mcpPath; } } } catch { } } // 4. Library/PackageCache(UPMキャッシュ)- synapticパッケージのみ検索 string packageCachePath = Path.Combine(projectPath, "Library", "PackageCache"); if (Directory.Exists(packageCachePath)) { try { foreach (var dir in Directory.GetDirectories(packageCachePath, "com.synaptic*", SearchOption.TopDirectoryOnly)) { var mcpPath = Path.Combine(dir, "MCPServer"); if (Directory.Exists(mcpPath)) { SynLog.Info($"[Synaptic] Found MCPServer in PackageCache: {mcpPath}"); _cachedMCPServerPath = mcpPath; return mcpPath; } } } catch { } } // 5. フォールバック string defaultPath = Path.Combine(Application.dataPath, "Synaptic AI Pro", "MCPServer"); SynLog.Warn($"[Synaptic] MCPServer not found, using default path: {defaultPath}"); _cachedMCPServerPath = defaultPath; return defaultPath; } /// // ===== Auto-Update System ===== private static string PREF_LAST_UPDATE_CHECK => "SynapticPro_LastUpdateCheck"; private void CheckForUpdates() { if (updateCheckDone) return; // 1日1回チェック var lastCheck = EditorPrefs.GetString(PREF_LAST_UPDATE_CHECK, ""); var today = DateTime.Now.ToString("yyyy-MM-dd"); if (lastCheck == today) return; updateCheckDone = true; EditorPrefs.SetString(PREF_LAST_UPDATE_CHECK, today); var currentVersion = NexusVersion.Current; var dist = ENABLE_SELF_UPDATE ? "booth" : "assetstore"; System.Threading.ThreadPool.QueueUserWorkItem(_ => { try { using (var client = new System.Net.WebClient()) { client.Headers.Add("User-Agent", "SynapticAIPro-Unity"); var url = $"https://kawaii-agent-backend.vercel.app/api/synaptic/unity-version?v={currentVersion}&dist={dist}"; var json = client.DownloadString(url); // Simple JSON parsing if (json.Contains("\"updateAvailable\":true") || json.Contains("\"updateAvailable\": true")) { // Extract latestVersion var vMatch = System.Text.RegularExpressions.Regex.Match(json, "\"latestVersion\"\\s*:\\s*\"([^\"]+)\""); var urlMatch = System.Text.RegularExpressions.Regex.Match(json, "\"updateUrl\"\\s*:\\s*\"([^\"]+)\""); var methodMatch = System.Text.RegularExpressions.Regex.Match(json, "\"updateMethod\"\\s*:\\s*\"([^\"]+)\""); if (vMatch.Success) { latestVersion = vMatch.Groups[1].Value; updateUrl = urlMatch.Success ? urlMatch.Groups[1].Value : ""; updateMethod = methodMatch.Success ? methodMatch.Groups[1].Value : "browser"; updateAvailable = true; EditorApplication.delayCall += () => { SynLog.Info($"[Synaptic] Update available: {currentVersion} → {latestVersion}"); Repaint(); }; } } } } catch (Exception e) { // フェイルサイレント SynLog.Info($"[Synaptic] Update check failed (non-critical): {e.Message}"); } }); } private void DrawUpdateBanner() { if (!updateAvailable) return; EditorGUILayout.BeginHorizontal(EditorStyles.helpBox); var oldColor = GUI.backgroundColor; GUI.backgroundColor = new Color(0.2f, 0.6f, 1f); EditorGUILayout.LabelField( $"🔄 v{latestVersion} available (current: {NexusVersion.Current})", EditorStyles.boldLabel, GUILayout.ExpandWidth(true) ); if (updateMethod == "auto" && ENABLE_SELF_UPDATE) { if (GUILayout.Button("Update Now", GUILayout.Width(100), GUILayout.Height(22))) { StartAutoUpdate(); } } else { if (GUILayout.Button("Open Store", GUILayout.Width(100), GUILayout.Height(22))) { Application.OpenURL(updateUrl); } } GUI.backgroundColor = oldColor; EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(5); } private async void StartAutoUpdate() { if (isDownloadingUpdate) return; isDownloadingUpdate = true; try { SynLog.Info($"[Synaptic] Downloading update v{latestVersion}..."); // ダウンロード先 var downloadPath = Path.Combine(Path.GetTempPath(), $"SynapticAIPro_v{latestVersion}.unitypackage.zip"); await Task.Run(() => { using (var client = new System.Net.WebClient()) { client.Headers.Add("User-Agent", "SynapticAIPro-Unity"); client.DownloadFile(updateUrl, downloadPath); } }); SynLog.Info($"[Synaptic] Downloaded to: {downloadPath}"); // ZIPを展開してunitypackageを取得 var extractDir = Path.Combine(Path.GetTempPath(), "SynapticUpdate"); if (Directory.Exists(extractDir)) Directory.Delete(extractDir, true); // System.IO.Compression でZIP展開 System.IO.Compression.ZipFile.ExtractToDirectory(downloadPath, extractDir); // .unitypackageファイルを探す var packages = Directory.GetFiles(extractDir, "*.unitypackage", SearchOption.AllDirectories); if (packages.Length > 0) { var packagePath = packages[0]; SynLog.Info($"[Synaptic] Importing update package: {packagePath}"); var synapticPath = Path.Combine(Application.dataPath, "Synaptic AI Pro"); // ===== セーフガード ===== // 1. unitypackageファイルサイズ妥当性チェック var pkgInfo = new FileInfo(packagePath); if (pkgInfo.Length < 100_000) // < 100KB { EditorUtility.DisplayDialog("Update Failed", "ダウンロードしたファイルが破損している可能性があります。手動で再ダウンロードしてください。", "OK"); return; } // 2. パス妥当性チェック - 正確に "Synaptic AI Pro" フォルダのみ操作 var fullPath = Path.GetFullPath(synapticPath); var assetsFullPath = Path.GetFullPath(Application.dataPath); // パスが Assets/Synaptic AI Pro として整合してるか厳密確認 if (!fullPath.StartsWith(assetsFullPath, System.StringComparison.OrdinalIgnoreCase) || !fullPath.EndsWith("Synaptic AI Pro", System.StringComparison.OrdinalIgnoreCase) || fullPath.Equals(assetsFullPath, System.StringComparison.OrdinalIgnoreCase)) { EditorUtility.DisplayDialog("Update Failed", "アップデート対象フォルダの検証に失敗しました。\n手動で再インストールしてください。", "OK"); return; } // 3. 既存フォルダが Synaptic AI Pro として整合してるか確認 (Editor配下に既知ファイルがある) if (Directory.Exists(synapticPath)) { var marker = Path.Combine(synapticPath, "Editor", "NexusSetupWindow.cs"); if (!File.Exists(marker)) { EditorUtility.DisplayDialog("Update Failed", "アップデート対象フォルダが Synaptic AI Pro のインストール先として認識できません。\n別のフォルダにインストールされている可能性があります。手動で再インストールしてください。", "OK"); return; } Directory.Delete(synapticPath, true); // .metaも削除 var metaPath = synapticPath + ".meta"; if (File.Exists(metaPath)) File.Delete(metaPath); } // インポート AssetDatabase.ImportPackage(packagePath, false); // false = 確認ダイアログなし AssetDatabase.Refresh(); updateAvailable = false; SynLog.Info($"[Synaptic] Update to v{latestVersion} complete!"); EditorUtility.DisplayDialog("Update Complete", $"Synaptic AI Pro has been updated to v{latestVersion}.\nPlease restart Unity for changes to take effect.", "OK"); } else { Debug.LogError("[Synaptic] No .unitypackage found in downloaded ZIP"); } // クリーンアップ try { File.Delete(downloadPath); } catch { } try { Directory.Delete(extractDir, true); } catch { } } catch (Exception e) { Debug.LogError($"[Synaptic] Auto-update failed: {e.Message}"); EditorUtility.DisplayDialog("Update Failed", $"Auto-update failed: {e.Message}\n\nPlease download manually from BOOTH.", "OK"); } finally { isDownloadingUpdate = false; } } /// /// Find Node.js executable path across different OS and installation locations /// Supports Windows D: drive installations, nvm, volta, and standard locations /// private string FindNodePath() { string nodeExecutable = Application.platform == RuntimePlatform.WindowsEditor ? "node.exe" : "node"; // 1. Check PATH environment variable first var pathEnv = Environment.GetEnvironmentVariable("PATH"); if (!string.IsNullOrEmpty(pathEnv)) { var separator = Application.platform == RuntimePlatform.WindowsEditor ? ';' : ':'; foreach (var path in pathEnv.Split(separator)) { if (!string.IsNullOrEmpty(path)) { var fullPath = Path.Combine(path.Trim(), nodeExecutable); if (File.Exists(fullPath)) { SynLog.Info($"[Synaptic] Found Node.js in PATH: {fullPath}"); return NormalizePathForJson(fullPath); } } } } // 2. Check common installation paths var searchPaths = new List(); if (Application.platform == RuntimePlatform.WindowsEditor) { // Windows standard locations var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); searchPaths.AddRange(new[] { // Standard Windows installations Path.Combine(programFiles, "nodejs"), Path.Combine(programFilesX86, "nodejs"), Path.Combine(localAppData, "Programs", "nodejs"), // nvm-windows Path.Combine(appData, "nvm"), Path.Combine(userProfile, ".nvm"), // volta Path.Combine(localAppData, "Volta", "bin"), Path.Combine(userProfile, ".volta", "bin"), // fnm Path.Combine(localAppData, "fnm"), Path.Combine(userProfile, ".fnm"), // Common D: drive installations (for users who install on secondary drives) "D:\\nodejs", "D:\\Program Files\\nodejs", "D:\\Program Files (x86)\\nodejs", "D:\\nvm", "D:\\nvm\\nodejs", // E: drive (some users use this too) "E:\\nodejs", "E:\\Program Files\\nodejs", }); } else { // macOS/Linux paths var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); searchPaths.AddRange(new[] { "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", Path.Combine(home, ".nvm", "versions", "node"), Path.Combine(home, ".volta", "bin"), Path.Combine(home, ".fnm"), Path.Combine(home, ".npm-global", "bin"), }); } foreach (var basePath in searchPaths) { if (Directory.Exists(basePath)) { // Direct check var directPath = Path.Combine(basePath, nodeExecutable); if (File.Exists(directPath)) { SynLog.Info($"[Synaptic] Found Node.js: {directPath}"); return NormalizePathForJson(directPath); } // Check subdirectories (for version managers like nvm) try { foreach (var subdir in Directory.GetDirectories(basePath)) { var binPath = Path.Combine(subdir, nodeExecutable); if (File.Exists(binPath)) { SynLog.Info($"[Synaptic] Found Node.js in version manager: {binPath}"); return NormalizePathForJson(binPath); } // Also check bin subdirectory binPath = Path.Combine(subdir, "bin", nodeExecutable); if (File.Exists(binPath)) { SynLog.Info($"[Synaptic] Found Node.js in version manager: {binPath}"); return NormalizePathForJson(binPath); } } } catch { } } } // 3. Try 'where' command on Windows, 'which' on Unix try { var process = new System.Diagnostics.Process { StartInfo = new System.Diagnostics.ProcessStartInfo { FileName = Application.platform == RuntimePlatform.WindowsEditor ? "where" : "which", Arguments = "node", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true } }; process.Start(); var output = process.StandardOutput.ReadLine()?.Trim(); process.WaitForExit(); if (process.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output)) { SynLog.Info($"[Synaptic] Found Node.js via system command: {output}"); return NormalizePathForJson(output); } } catch { } // 4. Return "node" as fallback (rely on PATH) SynLog.Warn("[Synaptic] Node.js path not detected, using 'node' (requires PATH to be set correctly)"); return "node"; } private bool CheckAndInstallDependencies() { try { SynLog.Info("[Synaptic] Checking dependencies..."); // Check if Newtonsoft.Json is installed var listRequest = Client.List(); while (!listRequest.IsCompleted) { System.Threading.Thread.Sleep(10); } if (listRequest.Status == StatusCode.Success) { bool hasNewtonsoft = false; foreach (var package in listRequest.Result) { if (package.name == "com.unity.nuget.newtonsoft-json") { hasNewtonsoft = true; SynLog.Info($"[Synaptic] Newtonsoft.Json already installed: v{package.version}"); break; } } if (!hasNewtonsoft) { SynLog.Info("[Synaptic] Installing Newtonsoft.Json..."); // Install Newtonsoft.Json var addRequest = Client.Add("com.unity.nuget.newtonsoft-json"); while (!addRequest.IsCompleted) { System.Threading.Thread.Sleep(10); } if (addRequest.Status == StatusCode.Success) { SynLog.Info("[Synaptic] Newtonsoft.Json installed successfully"); // Wait for compilation EditorUtility.DisplayDialog( "Dependencies Installed", "Newtonsoft.Json has been installed. Unity will now recompile.\n\nPlease run setup again after compilation completes.", "OK" ); return false; // Return false to stop setup and wait for recompilation } else if (addRequest.Status == StatusCode.Failure) { Debug.LogError($"[Synaptic] Failed to install Newtonsoft.Json: {addRequest.Error.message}"); return false; } } } // Check TextMeshPro bool hasTMP = false; if (listRequest.Status == StatusCode.Success) { foreach (var package in listRequest.Result) { if (package.name == "com.unity.textmeshpro") { hasTMP = true; SynLog.Info($"[Synaptic] TextMeshPro already installed: v{package.version}"); break; } } } if (!hasTMP) { SynLog.Warn("[Synaptic] TextMeshPro not found. Some features may not work properly."); } return true; } catch (Exception e) { Debug.LogError($"[Synaptic] Dependency check error: {e.Message}"); return true; // Continue anyway } } } }