97ac0f71f5
https://assetstore.unity.com/packages/tools/generative-ai/synaptic-ai-pro-natural-language-control-for-unity-336030
4814 lines
198 KiB
C#
4814 lines
198 KiB
C#
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
|
||
{
|
||
/// <summary>
|
||
/// MCP Server Setup and Local CLI Management Window
|
||
/// One-touch setup for MCP server and configuration of various AI tools
|
||
/// </summary>
|
||
public class NexusMCPSetupWindow : EditorWindow
|
||
{
|
||
[MenuItem("Tools/Synaptic Pro/Synaptic Setup", false, 0)]
|
||
public static void ShowWindow()
|
||
{
|
||
var window = GetWindow<NexusMCPSetupWindow>("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を開かなくても通知)
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
[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<NexusMCPSetupWindow>("Synaptic Setup", true);
|
||
}
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// Static port-alive probe usable from [InitializeOnLoadMethod].
|
||
/// Lightweight TCP connect with short timeout.
|
||
/// </summary>
|
||
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; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// Reconnect WebSocket to an already-running HTTP server after domain
|
||
/// reload. Does not start the server or open any UI.
|
||
/// </summary>
|
||
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<NexusMCPSetupWindow>("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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
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 </dev/null &";
|
||
|
||
startInfo = new System.Diagnostics.ProcessStartInfo
|
||
{
|
||
FileName = "/bin/sh",
|
||
Arguments = $"-c \"{shellCmd.Replace("\"", "\\\"")}\"",
|
||
WorkingDirectory = mcpServerPath,
|
||
UseShellExecute = false,
|
||
RedirectStandardOutput = false,
|
||
RedirectStandardError = false,
|
||
CreateNoWindow = true,
|
||
};
|
||
startInfo.EnvironmentVariables["HTTP_PORT"] = httpPort.ToString();
|
||
|
||
var detachProc = new System.Diagnostics.Process { StartInfo = startInfo };
|
||
detachProc.Start();
|
||
detachProc.WaitForExit(); // sh exits immediately after backgrounding node
|
||
detachProc.Dispose();
|
||
|
||
// We have no handle to the actual node process — by design,
|
||
// so that domain reload can't kill it. Recovery on next
|
||
// load uses port-listen probe in RestoreDetachedHttpServerOnReload.
|
||
externalHttpProcess = null;
|
||
externalHttpRunning = true;
|
||
EditorPrefs.SetInt(PREF_HTTP_PORT, httpPort);
|
||
SynLog.Info($"[Synaptic] External HTTP Server detach-spawned on port {httpPort}");
|
||
SynLog.Info($"[Synaptic] Node: {nodePath}");
|
||
SynLog.Info($"[Synaptic] Script: {httpServerScript}");
|
||
SynLog.Info($"[Synaptic] Log: {logFile}");
|
||
|
||
_ = ConnectToHttpServerAsync(httpPort);
|
||
return;
|
||
}
|
||
|
||
externalHttpProcess = new System.Diagnostics.Process { StartInfo = startInfo };
|
||
|
||
externalHttpProcess.OutputDataReceived += (sender, e) =>
|
||
{
|
||
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<Dictionary<string, object>>(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<string>();
|
||
|
||
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<string> DetectInstalledAIs()
|
||
{
|
||
var installedAIs = new List<string>();
|
||
|
||
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<Dictionary<string, object>>(
|
||
JsonConvert.SerializeObject(existingConfig.mcpServers));
|
||
mcpServers["unity-synaptic"] = unityMcpServer;
|
||
|
||
claudeConfig = new
|
||
{
|
||
mcpServers = mcpServers
|
||
};
|
||
|
||
// Preserve other existing settings
|
||
var configDict = JsonConvert.DeserializeObject<Dictionary<string, object>>(
|
||
JsonConvert.SerializeObject(existingConfig));
|
||
configDict["mcpServers"] = mcpServers;
|
||
claudeConfig = configDict;
|
||
}
|
||
else
|
||
{
|
||
// Create new configuration
|
||
claudeConfig = new
|
||
{
|
||
mcpServers = new Dictionary<string, object>
|
||
{
|
||
["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<string>
|
||
{
|
||
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<Dictionary<string, object>>(
|
||
JsonConvert.SerializeObject(existingConfig.mcpServers));
|
||
mcpServers["unity-synaptic"] = unityMcpServer;
|
||
|
||
var configDict = JsonConvert.DeserializeObject<Dictionary<string, object>>(
|
||
JsonConvert.SerializeObject(existingConfig));
|
||
configDict["mcpServers"] = mcpServers;
|
||
cursorConfig = configDict;
|
||
}
|
||
else
|
||
{
|
||
cursorConfig = new
|
||
{
|
||
mcpServers = new Dictionary<string, object>
|
||
{
|
||
["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<Dictionary<string, object>>(
|
||
JsonConvert.SerializeObject(existingConfig.servers));
|
||
servers["unity-synaptic"] = unityMcpServer;
|
||
|
||
var configDict = JsonConvert.DeserializeObject<Dictionary<string, object>>(
|
||
JsonConvert.SerializeObject(existingConfig));
|
||
configDict["servers"] = servers;
|
||
vscodeConfig = configDict;
|
||
}
|
||
else
|
||
{
|
||
vscodeConfig = new
|
||
{
|
||
servers = new Dictionary<string, object>
|
||
{
|
||
["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<Dictionary<string, object>>(
|
||
JsonConvert.SerializeObject(existingConfig.mcpServers));
|
||
mcpServers["unity-synaptic"] = unityMcpServer;
|
||
|
||
var configDict = JsonConvert.DeserializeObject<Dictionary<string, object>>(
|
||
JsonConvert.SerializeObject(existingConfig));
|
||
configDict["mcpServers"] = mcpServers;
|
||
lmstudioConfig = configDict;
|
||
}
|
||
else
|
||
{
|
||
lmstudioConfig = new
|
||
{
|
||
mcpServers = new Dictionary<string, object>
|
||
{
|
||
["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<Dictionary<string, object>>(
|
||
JsonConvert.SerializeObject(existingConfig.mcpServers));
|
||
mcpServers["unity-synaptic"] = unityMcpServer;
|
||
|
||
// Preserve other existing settings
|
||
var configDict = JsonConvert.DeserializeObject<Dictionary<string, object>>(
|
||
JsonConvert.SerializeObject(existingConfig));
|
||
configDict["mcpServers"] = mcpServers;
|
||
chatgptConfig = configDict;
|
||
}
|
||
else
|
||
{
|
||
// Create new configuration
|
||
chatgptConfig = new
|
||
{
|
||
mcpServers = new Dictionary<string, object>
|
||
{
|
||
["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<string>
|
||
{
|
||
$"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<string, object>(),
|
||
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<Dictionary<string, object>>(
|
||
JsonConvert.SerializeObject(existingConfig.servers));
|
||
servers["unity-synaptic"] = unityMcpServer;
|
||
|
||
var configDict = JsonConvert.DeserializeObject<Dictionary<string, object>>(
|
||
JsonConvert.SerializeObject(existingConfig));
|
||
configDict["servers"] = servers;
|
||
windsurfConfig = configDict;
|
||
}
|
||
else
|
||
{
|
||
windsurfConfig = new
|
||
{
|
||
servers = new Dictionary<string, object>
|
||
{
|
||
["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<Dictionary<string, object>>(
|
||
JsonConvert.SerializeObject(existingConfig.mcpServers));
|
||
mcpServers["unity-synaptic"] = unityMcpServer;
|
||
|
||
var configDict = JsonConvert.DeserializeObject<Dictionary<string, object>>(
|
||
JsonConvert.SerializeObject(existingConfig));
|
||
configDict["mcpServers"] = mcpServers;
|
||
antigravityConfig = configDict;
|
||
}
|
||
else
|
||
{
|
||
antigravityConfig = new
|
||
{
|
||
mcpServers = new Dictionary<string, object>
|
||
{
|
||
["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<Dictionary<string, object>>(
|
||
JsonConvert.SerializeObject(existingConfig.mcpServers));
|
||
mcpServers["unity-synaptic"] = unityMcpServer;
|
||
|
||
var configDict = JsonConvert.DeserializeObject<Dictionary<string, object>>(
|
||
JsonConvert.SerializeObject(existingConfig));
|
||
configDict["mcpServers"] = mcpServers;
|
||
kiroConfig = configDict;
|
||
}
|
||
else
|
||
{
|
||
kiroConfig = new
|
||
{
|
||
mcpServers = new Dictionary<string, object>
|
||
{
|
||
["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<string>();
|
||
|
||
// 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<string, object>
|
||
{
|
||
["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<string, object>()
|
||
};
|
||
|
||
// 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<Dictionary<string, object>>(
|
||
JsonConvert.SerializeObject(existingConfig.mcpServers));
|
||
mcpServers["unity-synaptic"] = unityMcpServer;
|
||
|
||
// Update entire config
|
||
var configDict = JsonConvert.DeserializeObject<Dictionary<string, object>>(
|
||
JsonConvert.SerializeObject(existingConfig));
|
||
configDict["mcpServers"] = mcpServers;
|
||
claudeCodeConfig = configDict;
|
||
}
|
||
else
|
||
{
|
||
// Create new - 2025 MCP specification compliant
|
||
claudeCodeConfig = new
|
||
{
|
||
mcpServers = new Dictionary<string, object>
|
||
{
|
||
["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<Dictionary<string, object>>(
|
||
JsonConvert.SerializeObject(userConfig.mcpServers));
|
||
mcpServers["unity-synaptic"] = unityMcpServer;
|
||
|
||
var configDict = JsonConvert.DeserializeObject<Dictionary<string, object>>(
|
||
JsonConvert.SerializeObject(userConfig));
|
||
configDict["mcpServers"] = mcpServers;
|
||
userConfig = configDict;
|
||
}
|
||
else
|
||
{
|
||
userConfig = new
|
||
{
|
||
mcpServers = new Dictionary<string, object>
|
||
{
|
||
["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}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
// ===== 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;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Find Node.js executable path across different OS and installation locations
|
||
/// Supports Windows D: drive installations, nvm, volta, and standard locations
|
||
/// </summary>
|
||
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<string>();
|
||
|
||
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
|
||
}
|
||
}
|
||
|
||
}
|
||
} |