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
}
}
}
}