2025-03-20 19:24:31 +08:00
|
|
|
using System;
|
2025-07-30 04:01:17 +08:00
|
|
|
using System.Collections.Generic;
|
2025-07-29 01:45:07 +08:00
|
|
|
using System.Diagnostics;
|
2025-08-08 08:43:33 +08:00
|
|
|
using System.Security.Cryptography;
|
|
|
|
|
using System.Text;
|
|
|
|
|
using System.Net.Sockets;
|
|
|
|
|
using System.Net;
|
2025-04-08 18:14:13 +08:00
|
|
|
using System.IO;
|
2025-07-24 11:31:47 +08:00
|
|
|
using System.Linq;
|
2025-04-08 18:14:13 +08:00
|
|
|
using System.Runtime.InteropServices;
|
|
|
|
|
using Newtonsoft.Json;
|
|
|
|
|
using UnityEditor;
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
using UnityMcpBridge.Editor.Data;
|
2025-04-09 03:03:16 +08:00
|
|
|
using UnityMcpBridge.Editor.Helpers;
|
2025-04-08 18:14:13 +08:00
|
|
|
using UnityMcpBridge.Editor.Models;
|
2025-03-20 19:24:31 +08:00
|
|
|
|
2025-04-08 18:14:13 +08:00
|
|
|
namespace UnityMcpBridge.Editor.Windows
|
2025-03-20 19:24:31 +08:00
|
|
|
{
|
2025-04-08 18:14:13 +08:00
|
|
|
public class UnityMcpEditorWindow : EditorWindow
|
2025-03-20 19:24:31 +08:00
|
|
|
{
|
|
|
|
|
private bool isUnityBridgeRunning = false;
|
|
|
|
|
private Vector2 scrollPosition;
|
2025-04-09 19:43:01 +08:00
|
|
|
private string pythonServerInstallationStatus = "Not Installed";
|
|
|
|
|
private Color pythonServerInstallationStatusColor = Color.red;
|
2025-07-29 12:17:36 +08:00
|
|
|
private const int mcpPort = 6500; // MCP port (still hardcoded for MCP server)
|
2025-04-09 19:43:01 +08:00
|
|
|
private readonly McpClients mcpClients = new();
|
2025-08-08 08:43:33 +08:00
|
|
|
private bool autoRegisterEnabled;
|
|
|
|
|
private bool lastClientRegisteredOk;
|
|
|
|
|
private bool lastBridgeVerifiedOk;
|
|
|
|
|
private string pythonDirOverride = null;
|
2025-08-09 02:23:45 +08:00
|
|
|
private bool debugLogsEnabled;
|
2025-07-24 11:31:47 +08:00
|
|
|
|
|
|
|
|
// Script validation settings
|
|
|
|
|
private int validationLevelIndex = 1; // Default to Standard
|
|
|
|
|
private readonly string[] validationLevelOptions = new string[]
|
|
|
|
|
{
|
|
|
|
|
"Basic - Only syntax checks",
|
|
|
|
|
"Standard - Syntax + Unity practices",
|
|
|
|
|
"Comprehensive - All checks + semantic analysis",
|
|
|
|
|
"Strict - Full semantic validation (requires Roslyn)"
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// UI state
|
|
|
|
|
private int selectedClientIndex = 0;
|
2025-03-20 19:24:31 +08:00
|
|
|
|
|
|
|
|
[MenuItem("Window/Unity MCP")]
|
|
|
|
|
public static void ShowWindow()
|
|
|
|
|
{
|
2025-04-08 18:14:13 +08:00
|
|
|
GetWindow<UnityMcpEditorWindow>("MCP Editor");
|
2025-03-20 19:24:31 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnEnable()
|
|
|
|
|
{
|
2025-04-09 19:43:01 +08:00
|
|
|
UpdatePythonServerInstallationStatus();
|
|
|
|
|
|
2025-07-29 12:17:36 +08:00
|
|
|
// Refresh bridge status
|
2025-04-08 18:14:13 +08:00
|
|
|
isUnityBridgeRunning = UnityMcpBridge.IsRunning;
|
2025-08-08 08:43:33 +08:00
|
|
|
autoRegisterEnabled = EditorPrefs.GetBool("UnityMCP.AutoRegisterEnabled", true);
|
2025-08-09 02:23:45 +08:00
|
|
|
debugLogsEnabled = EditorPrefs.GetBool("UnityMCP.DebugLogs", false);
|
2025-03-20 19:24:31 +08:00
|
|
|
foreach (McpClient mcpClient in mcpClients.clients)
|
|
|
|
|
{
|
|
|
|
|
CheckMcpConfiguration(mcpClient);
|
|
|
|
|
}
|
2025-07-24 11:31:47 +08:00
|
|
|
|
|
|
|
|
// Load validation level setting
|
|
|
|
|
LoadValidationLevelSetting();
|
2025-08-08 08:43:33 +08:00
|
|
|
|
2025-08-12 23:32:51 +08:00
|
|
|
// First-run auto-setup only if Claude CLI is available
|
|
|
|
|
if (autoRegisterEnabled && !string.IsNullOrEmpty(ExecPath.ResolveClaude()))
|
2025-08-08 08:43:33 +08:00
|
|
|
{
|
|
|
|
|
AutoFirstRunSetup();
|
|
|
|
|
}
|
2025-03-20 19:24:31 +08:00
|
|
|
}
|
2025-07-29 01:45:07 +08:00
|
|
|
|
|
|
|
|
private void OnFocus()
|
|
|
|
|
{
|
2025-08-08 06:32:03 +08:00
|
|
|
// Refresh bridge running state on focus in case initialization completed after domain reload
|
|
|
|
|
isUnityBridgeRunning = UnityMcpBridge.IsRunning;
|
2025-08-02 11:11:04 +08:00
|
|
|
if (mcpClients.clients.Count > 0 && selectedClientIndex < mcpClients.clients.Count)
|
2025-07-29 01:45:07 +08:00
|
|
|
{
|
2025-08-02 11:11:04 +08:00
|
|
|
McpClient selectedClient = mcpClients.clients[selectedClientIndex];
|
|
|
|
|
CheckMcpConfiguration(selectedClient);
|
2025-07-29 01:45:07 +08:00
|
|
|
}
|
|
|
|
|
Repaint();
|
|
|
|
|
}
|
2025-03-20 19:24:31 +08:00
|
|
|
|
|
|
|
|
private Color GetStatusColor(McpStatus status)
|
|
|
|
|
{
|
|
|
|
|
// Return appropriate color based on the status enum
|
|
|
|
|
return status switch
|
|
|
|
|
{
|
|
|
|
|
McpStatus.Configured => Color.green,
|
|
|
|
|
McpStatus.Running => Color.green,
|
|
|
|
|
McpStatus.Connected => Color.green,
|
|
|
|
|
McpStatus.IncorrectPath => Color.yellow,
|
|
|
|
|
McpStatus.CommunicationError => Color.yellow,
|
|
|
|
|
McpStatus.NoResponse => Color.yellow,
|
2025-04-08 18:14:13 +08:00
|
|
|
_ => Color.red, // Default to red for error states or not configured
|
2025-03-20 19:24:31 +08:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-09 19:43:01 +08:00
|
|
|
private void UpdatePythonServerInstallationStatus()
|
|
|
|
|
{
|
2025-08-09 05:16:25 +08:00
|
|
|
try
|
2025-04-09 19:43:01 +08:00
|
|
|
{
|
2025-08-09 05:16:25 +08:00
|
|
|
string installedPath = ServerInstaller.GetServerPath();
|
|
|
|
|
bool installedOk = !string.IsNullOrEmpty(installedPath) && File.Exists(Path.Combine(installedPath, "server.py"));
|
|
|
|
|
if (installedOk)
|
|
|
|
|
{
|
|
|
|
|
pythonServerInstallationStatus = "Installed";
|
|
|
|
|
pythonServerInstallationStatusColor = Color.green;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fall back to embedded/dev source via our existing resolution logic
|
|
|
|
|
string embeddedPath = FindPackagePythonDirectory();
|
|
|
|
|
bool embeddedOk = !string.IsNullOrEmpty(embeddedPath) && File.Exists(Path.Combine(embeddedPath, "server.py"));
|
|
|
|
|
if (embeddedOk)
|
|
|
|
|
{
|
|
|
|
|
pythonServerInstallationStatus = "Installed (Embedded)";
|
|
|
|
|
pythonServerInstallationStatusColor = Color.green;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
pythonServerInstallationStatus = "Not Installed";
|
|
|
|
|
pythonServerInstallationStatusColor = Color.red;
|
|
|
|
|
}
|
2025-04-09 19:43:01 +08:00
|
|
|
}
|
2025-08-09 05:16:25 +08:00
|
|
|
catch
|
2025-04-09 19:43:01 +08:00
|
|
|
{
|
|
|
|
|
pythonServerInstallationStatus = "Not Installed";
|
|
|
|
|
pythonServerInstallationStatusColor = Color.red;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-24 11:31:47 +08:00
|
|
|
|
|
|
|
|
private void DrawStatusDot(Rect statusRect, Color statusColor, float size = 12)
|
2025-03-20 19:24:31 +08:00
|
|
|
{
|
2025-07-24 11:31:47 +08:00
|
|
|
float offsetX = (statusRect.width - size) / 2;
|
|
|
|
|
float offsetY = (statusRect.height - size) / 2;
|
|
|
|
|
Rect dotRect = new(statusRect.x + offsetX, statusRect.y + offsetY, size, size);
|
|
|
|
|
Vector3 center = new(
|
|
|
|
|
dotRect.x + (dotRect.width / 2),
|
|
|
|
|
dotRect.y + (dotRect.height / 2),
|
|
|
|
|
0
|
|
|
|
|
);
|
|
|
|
|
float radius = size / 2;
|
2025-03-20 19:24:31 +08:00
|
|
|
|
2025-07-24 11:31:47 +08:00
|
|
|
// Draw the main dot
|
|
|
|
|
Handles.color = statusColor;
|
|
|
|
|
Handles.DrawSolidDisc(center, Vector3.forward, radius);
|
2025-03-20 19:24:31 +08:00
|
|
|
|
2025-07-24 11:31:47 +08:00
|
|
|
// Draw the border
|
|
|
|
|
Color borderColor = new(
|
|
|
|
|
statusColor.r * 0.7f,
|
|
|
|
|
statusColor.g * 0.7f,
|
|
|
|
|
statusColor.b * 0.7f
|
|
|
|
|
);
|
|
|
|
|
Handles.color = borderColor;
|
|
|
|
|
Handles.DrawWireDisc(center, Vector3.forward, radius);
|
|
|
|
|
}
|
2025-03-20 19:24:31 +08:00
|
|
|
|
2025-07-24 11:31:47 +08:00
|
|
|
private void OnGUI()
|
|
|
|
|
{
|
|
|
|
|
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
|
|
|
|
|
|
|
|
|
|
// Header
|
|
|
|
|
DrawHeader();
|
|
|
|
|
|
2025-08-09 02:23:45 +08:00
|
|
|
// Compute equal column widths for uniform layout
|
|
|
|
|
float horizontalSpacing = 2f;
|
|
|
|
|
float outerPadding = 20f; // approximate padding
|
|
|
|
|
// Make columns a bit less wide for a tighter layout
|
|
|
|
|
float computed = (position.width - outerPadding - horizontalSpacing) / 2f;
|
|
|
|
|
float colWidth = Mathf.Clamp(computed, 220f, 340f);
|
|
|
|
|
// Use fixed heights per row so paired panels match exactly
|
|
|
|
|
float topPanelHeight = 190f;
|
|
|
|
|
float bottomPanelHeight = 230f;
|
|
|
|
|
|
|
|
|
|
// Top row: Server Status (left) and Unity Bridge (right)
|
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
|
|
|
{
|
|
|
|
|
EditorGUILayout.BeginVertical(GUILayout.Width(colWidth), GUILayout.Height(topPanelHeight));
|
|
|
|
|
DrawServerStatusSection();
|
|
|
|
|
EditorGUILayout.EndVertical();
|
|
|
|
|
|
|
|
|
|
EditorGUILayout.Space(horizontalSpacing);
|
|
|
|
|
|
|
|
|
|
EditorGUILayout.BeginVertical(GUILayout.Width(colWidth), GUILayout.Height(topPanelHeight));
|
|
|
|
|
DrawBridgeSection();
|
|
|
|
|
EditorGUILayout.EndVertical();
|
|
|
|
|
}
|
|
|
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
|
|
2025-08-08 08:43:33 +08:00
|
|
|
EditorGUILayout.Space(10);
|
2025-08-09 02:23:45 +08:00
|
|
|
|
|
|
|
|
// Second row: MCP Client Configuration (left) and Script Validation (right)
|
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
|
|
|
{
|
|
|
|
|
EditorGUILayout.BeginVertical(GUILayout.Width(colWidth), GUILayout.Height(bottomPanelHeight));
|
|
|
|
|
DrawUnifiedClientConfiguration();
|
|
|
|
|
EditorGUILayout.EndVertical();
|
|
|
|
|
|
|
|
|
|
EditorGUILayout.Space(horizontalSpacing);
|
|
|
|
|
|
|
|
|
|
EditorGUILayout.BeginVertical(GUILayout.Width(colWidth), GUILayout.Height(bottomPanelHeight));
|
|
|
|
|
DrawValidationSection();
|
|
|
|
|
EditorGUILayout.EndVertical();
|
|
|
|
|
}
|
|
|
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
|
|
|
|
|
|
// Minimal bottom padding
|
|
|
|
|
EditorGUILayout.Space(2);
|
2025-07-24 11:31:47 +08:00
|
|
|
|
|
|
|
|
EditorGUILayout.EndScrollView();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void DrawHeader()
|
|
|
|
|
{
|
|
|
|
|
EditorGUILayout.Space(15);
|
|
|
|
|
Rect titleRect = EditorGUILayout.GetControlRect(false, 40);
|
|
|
|
|
EditorGUI.DrawRect(titleRect, new Color(0.2f, 0.2f, 0.2f, 0.1f));
|
|
|
|
|
|
|
|
|
|
GUIStyle titleStyle = new GUIStyle(EditorStyles.boldLabel)
|
|
|
|
|
{
|
|
|
|
|
fontSize = 16,
|
|
|
|
|
alignment = TextAnchor.MiddleLeft
|
|
|
|
|
};
|
|
|
|
|
|
2025-04-08 18:14:13 +08:00
|
|
|
GUI.Label(
|
2025-07-24 11:31:47 +08:00
|
|
|
new Rect(titleRect.x + 15, titleRect.y + 8, titleRect.width - 30, titleRect.height),
|
|
|
|
|
"Unity MCP Editor",
|
|
|
|
|
titleStyle
|
2025-04-08 18:14:13 +08:00
|
|
|
);
|
2025-08-09 05:16:25 +08:00
|
|
|
|
|
|
|
|
// Place the Show Debug Logs toggle on the same header row, right-aligned
|
|
|
|
|
float toggleWidth = 160f;
|
|
|
|
|
Rect toggleRect = new Rect(titleRect.xMax - toggleWidth - 12f, titleRect.y + 10f, toggleWidth, 20f);
|
|
|
|
|
bool newDebug = GUI.Toggle(toggleRect, debugLogsEnabled, "Show Debug Logs");
|
|
|
|
|
if (newDebug != debugLogsEnabled)
|
|
|
|
|
{
|
|
|
|
|
debugLogsEnabled = newDebug;
|
|
|
|
|
EditorPrefs.SetBool("UnityMCP.DebugLogs", debugLogsEnabled);
|
|
|
|
|
}
|
2025-07-24 11:31:47 +08:00
|
|
|
EditorGUILayout.Space(15);
|
|
|
|
|
}
|
2025-03-20 19:24:31 +08:00
|
|
|
|
2025-07-24 11:31:47 +08:00
|
|
|
private void DrawServerStatusSection()
|
|
|
|
|
{
|
|
|
|
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
|
|
|
|
|
|
|
|
|
GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel)
|
|
|
|
|
{
|
|
|
|
|
fontSize = 14
|
|
|
|
|
};
|
|
|
|
|
EditorGUILayout.LabelField("Server Status", sectionTitleStyle);
|
|
|
|
|
EditorGUILayout.Space(8);
|
|
|
|
|
|
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
|
|
|
Rect statusRect = GUILayoutUtility.GetRect(0, 28, GUILayout.Width(24));
|
|
|
|
|
DrawStatusDot(statusRect, pythonServerInstallationStatusColor, 16);
|
|
|
|
|
|
|
|
|
|
GUIStyle statusStyle = new GUIStyle(EditorStyles.label)
|
|
|
|
|
{
|
|
|
|
|
fontSize = 12,
|
|
|
|
|
fontStyle = FontStyle.Bold
|
|
|
|
|
};
|
|
|
|
|
EditorGUILayout.LabelField(pythonServerInstallationStatus, statusStyle, GUILayout.Height(28));
|
|
|
|
|
EditorGUILayout.EndHorizontal();
|
2025-03-20 19:24:31 +08:00
|
|
|
|
2025-07-24 11:31:47 +08:00
|
|
|
EditorGUILayout.Space(5);
|
2025-07-29 12:17:36 +08:00
|
|
|
|
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
|
|
|
bool isAutoMode = UnityMcpBridge.IsAutoConnectMode();
|
|
|
|
|
GUIStyle modeStyle = new GUIStyle(EditorStyles.miniLabel) { fontSize = 11 };
|
|
|
|
|
EditorGUILayout.LabelField($"Mode: {(isAutoMode ? "Auto" : "Standard")}", modeStyle);
|
2025-08-08 08:43:33 +08:00
|
|
|
GUILayout.FlexibleSpace();
|
2025-07-29 12:17:36 +08:00
|
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
|
|
|
|
|
|
int currentUnityPort = UnityMcpBridge.GetCurrentPort();
|
2025-07-24 11:31:47 +08:00
|
|
|
GUIStyle portStyle = new GUIStyle(EditorStyles.miniLabel)
|
|
|
|
|
{
|
|
|
|
|
fontSize = 11
|
|
|
|
|
};
|
2025-07-29 12:17:36 +08:00
|
|
|
EditorGUILayout.LabelField($"Ports: Unity {currentUnityPort}, MCP {mcpPort}", portStyle);
|
2025-07-24 11:31:47 +08:00
|
|
|
EditorGUILayout.Space(5);
|
2025-08-08 08:43:33 +08:00
|
|
|
|
2025-08-09 05:16:25 +08:00
|
|
|
/// Auto-Setup button below ports
|
2025-08-09 02:23:45 +08:00
|
|
|
string setupButtonText = (lastClientRegisteredOk && lastBridgeVerifiedOk) ? "Connected ✓" : "Auto-Setup";
|
|
|
|
|
if (GUILayout.Button(setupButtonText, GUILayout.Height(24)))
|
|
|
|
|
{
|
|
|
|
|
RunSetupNow();
|
|
|
|
|
}
|
|
|
|
|
EditorGUILayout.Space(4);
|
|
|
|
|
|
2025-08-09 05:16:25 +08:00
|
|
|
// Repair Python Env button with tooltip tag
|
2025-08-09 02:23:45 +08:00
|
|
|
using (new EditorGUILayout.HorizontalScope())
|
|
|
|
|
{
|
|
|
|
|
GUILayout.FlexibleSpace();
|
2025-08-09 05:16:25 +08:00
|
|
|
GUIContent repairLabel = new GUIContent(
|
|
|
|
|
"Repair Python Env",
|
|
|
|
|
"Deletes the server's .venv and runs 'uv sync' to rebuild a clean environment. Use this if modules are missing or Python upgraded."
|
|
|
|
|
);
|
|
|
|
|
if (GUILayout.Button(repairLabel, GUILayout.Width(160), GUILayout.Height(22)))
|
2025-08-09 02:23:45 +08:00
|
|
|
{
|
2025-08-09 05:16:25 +08:00
|
|
|
bool ok = global::UnityMcpBridge.Editor.Helpers.ServerInstaller.RepairPythonEnvironment();
|
|
|
|
|
if (ok)
|
|
|
|
|
{
|
|
|
|
|
EditorUtility.DisplayDialog("Unity MCP", "Python environment repaired.", "OK");
|
|
|
|
|
UpdatePythonServerInstallationStatus();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
EditorUtility.DisplayDialog("Unity MCP", "Repair failed. Please check Console for details.", "OK");
|
|
|
|
|
}
|
2025-08-09 02:23:45 +08:00
|
|
|
}
|
|
|
|
|
}
|
2025-08-09 05:16:25 +08:00
|
|
|
// (Removed descriptive tool tag under the Repair button)
|
|
|
|
|
|
|
|
|
|
// (Show Debug Logs toggle moved to header)
|
2025-08-09 02:23:45 +08:00
|
|
|
EditorGUILayout.Space(2);
|
|
|
|
|
|
2025-08-09 05:16:25 +08:00
|
|
|
// Python detection warning with link
|
|
|
|
|
if (!IsPythonDetected())
|
|
|
|
|
{
|
|
|
|
|
GUIStyle warnStyle = new GUIStyle(EditorStyles.label) { richText = true, wordWrap = true };
|
|
|
|
|
EditorGUILayout.LabelField("<color=#cc3333><b>Warning:</b></color> No Python installation found.", warnStyle);
|
|
|
|
|
using (new EditorGUILayout.HorizontalScope())
|
|
|
|
|
{
|
|
|
|
|
if (GUILayout.Button("Open install instructions", GUILayout.Width(200)))
|
|
|
|
|
{
|
|
|
|
|
Application.OpenURL("https://www.python.org/downloads/");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
EditorGUILayout.Space(4);
|
|
|
|
|
}
|
2025-08-08 08:43:33 +08:00
|
|
|
|
|
|
|
|
// Troubleshooting helpers
|
|
|
|
|
if (pythonServerInstallationStatusColor != Color.green)
|
|
|
|
|
{
|
|
|
|
|
using (new EditorGUILayout.HorizontalScope())
|
|
|
|
|
{
|
|
|
|
|
if (GUILayout.Button("Select server folder…", GUILayout.Width(160)))
|
|
|
|
|
{
|
|
|
|
|
string picked = EditorUtility.OpenFolderPanel("Select UnityMcpServer/src", Application.dataPath, "");
|
|
|
|
|
if (!string.IsNullOrEmpty(picked) && File.Exists(Path.Combine(picked, "server.py")))
|
|
|
|
|
{
|
|
|
|
|
pythonDirOverride = picked;
|
|
|
|
|
EditorPrefs.SetString("UnityMCP.PythonDirOverride", pythonDirOverride);
|
|
|
|
|
UpdatePythonServerInstallationStatus();
|
|
|
|
|
}
|
|
|
|
|
else if (!string.IsNullOrEmpty(picked))
|
|
|
|
|
{
|
|
|
|
|
EditorUtility.DisplayDialog("Invalid Selection", "The selected folder does not contain server.py", "OK");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (GUILayout.Button("Verify again", GUILayout.Width(120)))
|
|
|
|
|
{
|
|
|
|
|
UpdatePythonServerInstallationStatus();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-24 11:31:47 +08:00
|
|
|
EditorGUILayout.EndVertical();
|
|
|
|
|
}
|
2025-03-20 19:24:31 +08:00
|
|
|
|
2025-07-24 11:31:47 +08:00
|
|
|
private void DrawBridgeSection()
|
|
|
|
|
{
|
|
|
|
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
|
|
|
|
|
2025-08-08 06:32:03 +08:00
|
|
|
// Always reflect the live state each repaint to avoid stale UI after recompiles
|
|
|
|
|
isUnityBridgeRunning = UnityMcpBridge.IsRunning;
|
|
|
|
|
|
2025-07-24 11:31:47 +08:00
|
|
|
GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel)
|
|
|
|
|
{
|
|
|
|
|
fontSize = 14
|
|
|
|
|
};
|
|
|
|
|
EditorGUILayout.LabelField("Unity Bridge", sectionTitleStyle);
|
|
|
|
|
EditorGUILayout.Space(8);
|
|
|
|
|
|
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
|
|
|
Color bridgeColor = isUnityBridgeRunning ? Color.green : Color.red;
|
|
|
|
|
Rect bridgeStatusRect = GUILayoutUtility.GetRect(0, 28, GUILayout.Width(24));
|
|
|
|
|
DrawStatusDot(bridgeStatusRect, bridgeColor, 16);
|
|
|
|
|
|
|
|
|
|
GUIStyle bridgeStatusStyle = new GUIStyle(EditorStyles.label)
|
|
|
|
|
{
|
|
|
|
|
fontSize = 12,
|
|
|
|
|
fontStyle = FontStyle.Bold
|
|
|
|
|
};
|
|
|
|
|
EditorGUILayout.LabelField(isUnityBridgeRunning ? "Running" : "Stopped", bridgeStatusStyle, GUILayout.Height(28));
|
2025-03-20 19:24:31 +08:00
|
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
|
|
|
|
|
|
EditorGUILayout.Space(8);
|
2025-07-24 11:31:47 +08:00
|
|
|
if (GUILayout.Button(isUnityBridgeRunning ? "Stop Bridge" : "Start Bridge", GUILayout.Height(32)))
|
|
|
|
|
{
|
|
|
|
|
ToggleUnityBridge();
|
|
|
|
|
}
|
|
|
|
|
EditorGUILayout.Space(5);
|
|
|
|
|
EditorGUILayout.EndVertical();
|
|
|
|
|
}
|
2025-03-20 19:24:31 +08:00
|
|
|
|
2025-07-24 11:31:47 +08:00
|
|
|
private void DrawValidationSection()
|
|
|
|
|
{
|
|
|
|
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
|
|
|
|
|
|
|
|
|
GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel)
|
2025-04-09 21:10:21 +08:00
|
|
|
{
|
2025-07-24 11:31:47 +08:00
|
|
|
fontSize = 14
|
2025-04-09 21:10:21 +08:00
|
|
|
};
|
2025-07-24 11:31:47 +08:00
|
|
|
EditorGUILayout.LabelField("Script Validation", sectionTitleStyle);
|
|
|
|
|
EditorGUILayout.Space(8);
|
|
|
|
|
|
|
|
|
|
EditorGUI.BeginChangeCheck();
|
|
|
|
|
validationLevelIndex = EditorGUILayout.Popup("Validation Level", validationLevelIndex, validationLevelOptions, GUILayout.Height(20));
|
|
|
|
|
if (EditorGUI.EndChangeCheck())
|
|
|
|
|
{
|
|
|
|
|
SaveValidationLevelSetting();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
EditorGUILayout.Space(8);
|
|
|
|
|
string description = GetValidationLevelDescription(validationLevelIndex);
|
|
|
|
|
EditorGUILayout.HelpBox(description, MessageType.Info);
|
2025-08-09 02:23:45 +08:00
|
|
|
EditorGUILayout.Space(4);
|
2025-08-09 05:16:25 +08:00
|
|
|
// (Show Debug Logs toggle moved to header)
|
2025-08-09 02:23:45 +08:00
|
|
|
EditorGUILayout.Space(2);
|
2025-07-24 11:31:47 +08:00
|
|
|
EditorGUILayout.EndVertical();
|
|
|
|
|
}
|
2025-03-20 19:24:31 +08:00
|
|
|
|
2025-07-24 11:31:47 +08:00
|
|
|
private void DrawUnifiedClientConfiguration()
|
|
|
|
|
{
|
|
|
|
|
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
|
|
|
|
|
|
|
|
|
GUIStyle sectionTitleStyle = new GUIStyle(EditorStyles.boldLabel)
|
|
|
|
|
{
|
|
|
|
|
fontSize = 14
|
|
|
|
|
};
|
|
|
|
|
EditorGUILayout.LabelField("MCP Client Configuration", sectionTitleStyle);
|
|
|
|
|
EditorGUILayout.Space(10);
|
|
|
|
|
|
2025-08-13 10:04:47 +08:00
|
|
|
// (Auto-connect toggle removed per design)
|
2025-08-08 08:43:33 +08:00
|
|
|
|
2025-07-24 11:31:47 +08:00
|
|
|
// Client selector
|
|
|
|
|
string[] clientNames = mcpClients.clients.Select(c => c.name).ToArray();
|
|
|
|
|
EditorGUI.BeginChangeCheck();
|
|
|
|
|
selectedClientIndex = EditorGUILayout.Popup("Select Client", selectedClientIndex, clientNames, GUILayout.Height(20));
|
|
|
|
|
if (EditorGUI.EndChangeCheck())
|
|
|
|
|
{
|
|
|
|
|
selectedClientIndex = Mathf.Clamp(selectedClientIndex, 0, mcpClients.clients.Count - 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
EditorGUILayout.Space(10);
|
|
|
|
|
|
|
|
|
|
if (mcpClients.clients.Count > 0 && selectedClientIndex < mcpClients.clients.Count)
|
|
|
|
|
{
|
|
|
|
|
McpClient selectedClient = mcpClients.clients[selectedClientIndex];
|
|
|
|
|
DrawClientConfigurationCompact(selectedClient);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
EditorGUILayout.Space(5);
|
|
|
|
|
EditorGUILayout.EndVertical();
|
|
|
|
|
}
|
2025-03-20 19:24:31 +08:00
|
|
|
|
2025-08-08 08:43:33 +08:00
|
|
|
private void AutoFirstRunSetup()
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
// Project-scoped one-time flag
|
|
|
|
|
string projectPath = Application.dataPath ?? string.Empty;
|
|
|
|
|
string key = $"UnityMCP.AutoRegistered.{ComputeSha1(projectPath)}";
|
|
|
|
|
if (EditorPrefs.GetBool(key, false))
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Attempt client registration using discovered Python server dir
|
|
|
|
|
pythonDirOverride ??= EditorPrefs.GetString("UnityMCP.PythonDirOverride", null);
|
|
|
|
|
string pythonDir = !string.IsNullOrEmpty(pythonDirOverride) ? pythonDirOverride : FindPackagePythonDirectory();
|
|
|
|
|
if (!string.IsNullOrEmpty(pythonDir) && File.Exists(Path.Combine(pythonDir, "server.py")))
|
|
|
|
|
{
|
|
|
|
|
bool anyRegistered = false;
|
|
|
|
|
foreach (McpClient client in mcpClients.clients)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (client.mcpType == McpTypes.ClaudeCode)
|
|
|
|
|
{
|
2025-08-12 23:32:51 +08:00
|
|
|
// Only attempt if Claude CLI is present
|
|
|
|
|
if (!IsClaudeConfigured() && !string.IsNullOrEmpty(ExecPath.ResolveClaude()))
|
2025-08-08 08:43:33 +08:00
|
|
|
{
|
|
|
|
|
RegisterWithClaudeCode(pythonDir);
|
|
|
|
|
anyRegistered = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// For Cursor/others, skip if already configured
|
|
|
|
|
if (!IsCursorConfigured(pythonDir))
|
|
|
|
|
{
|
|
|
|
|
ConfigureMcpClient(client);
|
|
|
|
|
anyRegistered = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
UnityEngine.Debug.LogWarning($"Auto-setup client '{client.name}' failed: {ex.Message}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
lastClientRegisteredOk = anyRegistered || IsCursorConfigured(pythonDir) || IsClaudeConfigured();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ensure the bridge is listening and has a fresh saved port
|
|
|
|
|
if (!UnityMcpBridge.IsRunning)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
UnityMcpBridge.StartAutoConnect();
|
|
|
|
|
isUnityBridgeRunning = UnityMcpBridge.IsRunning;
|
|
|
|
|
Repaint();
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
UnityEngine.Debug.LogWarning($"Auto-setup StartAutoConnect failed: {ex.Message}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify bridge with a quick ping
|
|
|
|
|
lastBridgeVerifiedOk = VerifyBridgePing(UnityMcpBridge.GetCurrentPort());
|
|
|
|
|
|
|
|
|
|
EditorPrefs.SetBool(key, true);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
|
|
|
|
UnityEngine.Debug.LogWarning($"Unity MCP auto-setup skipped: {e.Message}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string ComputeSha1(string input)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
using SHA1 sha1 = SHA1.Create();
|
|
|
|
|
byte[] bytes = Encoding.UTF8.GetBytes(input ?? string.Empty);
|
|
|
|
|
byte[] hash = sha1.ComputeHash(bytes);
|
|
|
|
|
StringBuilder sb = new StringBuilder(hash.Length * 2);
|
|
|
|
|
foreach (byte b in hash)
|
|
|
|
|
{
|
|
|
|
|
sb.Append(b.ToString("x2"));
|
|
|
|
|
}
|
|
|
|
|
return sb.ToString();
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void RunSetupNow()
|
|
|
|
|
{
|
|
|
|
|
// Force a one-shot setup regardless of first-run flag
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
pythonDirOverride ??= EditorPrefs.GetString("UnityMCP.PythonDirOverride", null);
|
|
|
|
|
string pythonDir = !string.IsNullOrEmpty(pythonDirOverride) ? pythonDirOverride : FindPackagePythonDirectory();
|
|
|
|
|
if (string.IsNullOrEmpty(pythonDir) || !File.Exists(Path.Combine(pythonDir, "server.py")))
|
|
|
|
|
{
|
|
|
|
|
EditorUtility.DisplayDialog("Setup", "Python server not found. Please select UnityMcpServer/src.", "OK");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool anyRegistered = false;
|
|
|
|
|
foreach (McpClient client in mcpClients.clients)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (client.mcpType == McpTypes.ClaudeCode)
|
|
|
|
|
{
|
|
|
|
|
if (!IsClaudeConfigured())
|
|
|
|
|
{
|
|
|
|
|
RegisterWithClaudeCode(pythonDir);
|
|
|
|
|
anyRegistered = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if (!IsCursorConfigured(pythonDir))
|
|
|
|
|
{
|
|
|
|
|
ConfigureMcpClient(client);
|
|
|
|
|
anyRegistered = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
UnityEngine.Debug.LogWarning($"Setup client '{client.name}' failed: {ex.Message}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
lastClientRegisteredOk = anyRegistered || IsCursorConfigured(pythonDir) || IsClaudeConfigured();
|
|
|
|
|
|
|
|
|
|
// Restart/ensure bridge
|
|
|
|
|
UnityMcpBridge.StartAutoConnect();
|
|
|
|
|
isUnityBridgeRunning = UnityMcpBridge.IsRunning;
|
|
|
|
|
|
|
|
|
|
// Verify
|
|
|
|
|
lastBridgeVerifiedOk = VerifyBridgePing(UnityMcpBridge.GetCurrentPort());
|
|
|
|
|
Repaint();
|
|
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
|
|
|
|
EditorUtility.DisplayDialog("Setup Failed", e.Message, "OK");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static bool IsCursorConfigured(string pythonDir)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
|
|
|
|
? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
|
|
|
|
".cursor", "mcp.json")
|
|
|
|
|
: Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
|
|
|
|
".cursor", "mcp.json");
|
|
|
|
|
if (!File.Exists(configPath)) return false;
|
|
|
|
|
string json = File.ReadAllText(configPath);
|
|
|
|
|
dynamic cfg = JsonConvert.DeserializeObject(json);
|
|
|
|
|
var servers = cfg?.mcpServers;
|
|
|
|
|
if (servers == null) return false;
|
|
|
|
|
var unity = servers.unityMCP ?? servers.UnityMCP;
|
|
|
|
|
if (unity == null) return false;
|
|
|
|
|
var args = unity.args;
|
|
|
|
|
if (args == null) return false;
|
|
|
|
|
foreach (var a in args)
|
|
|
|
|
{
|
|
|
|
|
string s = (string)a;
|
|
|
|
|
if (!string.IsNullOrEmpty(s) && s.Contains(pythonDir, StringComparison.Ordinal))
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
catch { return false; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static bool IsClaudeConfigured()
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2025-08-13 03:14:48 +08:00
|
|
|
string claudePath = ExecPath.ResolveClaude();
|
|
|
|
|
if (string.IsNullOrEmpty(claudePath)) return false;
|
|
|
|
|
|
|
|
|
|
// Only prepend PATH on Unix
|
|
|
|
|
string pathPrepend = null;
|
|
|
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
|
|
|
|
{
|
|
|
|
|
pathPrepend = RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
|
|
|
|
|
? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
|
|
|
|
|
: "/usr/local/bin:/usr/bin:/bin";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!ExecPath.TryRun(claudePath, "mcp list", workingDir: null, out var stdout, out var stderr, 5000, pathPrepend))
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return (stdout ?? string.Empty).IndexOf("UnityMCP", StringComparison.OrdinalIgnoreCase) >= 0;
|
2025-08-08 08:43:33 +08:00
|
|
|
}
|
|
|
|
|
catch { return false; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static bool VerifyBridgePing(int port)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
using TcpClient c = new TcpClient();
|
|
|
|
|
var task = c.ConnectAsync(IPAddress.Loopback, port);
|
|
|
|
|
if (!task.Wait(500)) return false;
|
|
|
|
|
using NetworkStream s = c.GetStream();
|
|
|
|
|
byte[] ping = Encoding.UTF8.GetBytes("ping");
|
|
|
|
|
s.Write(ping, 0, ping.Length);
|
|
|
|
|
s.ReadTimeout = 1000;
|
|
|
|
|
byte[] buf = new byte[256];
|
|
|
|
|
int n = s.Read(buf, 0, buf.Length);
|
|
|
|
|
if (n <= 0) return false;
|
|
|
|
|
string resp = Encoding.UTF8.GetString(buf, 0, n);
|
|
|
|
|
return resp.Contains("pong", StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
}
|
|
|
|
|
catch { return false; }
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-24 11:31:47 +08:00
|
|
|
private void DrawClientConfigurationCompact(McpClient mcpClient)
|
|
|
|
|
{
|
2025-08-13 10:04:47 +08:00
|
|
|
// Special pre-check for Claude Code: if CLI missing, reflect in status UI
|
|
|
|
|
if (mcpClient.mcpType == McpTypes.ClaudeCode)
|
|
|
|
|
{
|
|
|
|
|
string claudeCheck = ExecPath.ResolveClaude();
|
|
|
|
|
if (string.IsNullOrEmpty(claudeCheck))
|
|
|
|
|
{
|
|
|
|
|
mcpClient.configStatus = "Claude Not Found";
|
|
|
|
|
mcpClient.status = McpStatus.NotConfigured;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-13 11:56:22 +08:00
|
|
|
// Pre-check for clients that require uv (all except Claude Code)
|
|
|
|
|
bool uvRequired = mcpClient.mcpType != McpTypes.ClaudeCode;
|
|
|
|
|
bool uvMissingEarly = false;
|
|
|
|
|
if (uvRequired)
|
|
|
|
|
{
|
|
|
|
|
string uvPathEarly = FindUvPath();
|
|
|
|
|
if (string.IsNullOrEmpty(uvPathEarly))
|
|
|
|
|
{
|
|
|
|
|
uvMissingEarly = true;
|
|
|
|
|
mcpClient.configStatus = "uv Not Found";
|
|
|
|
|
mcpClient.status = McpStatus.NotConfigured;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-24 11:31:47 +08:00
|
|
|
// Status display
|
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
|
|
|
Rect statusRect = GUILayoutUtility.GetRect(0, 28, GUILayout.Width(24));
|
|
|
|
|
Color statusColor = GetStatusColor(mcpClient.status);
|
|
|
|
|
DrawStatusDot(statusRect, statusColor, 16);
|
|
|
|
|
|
|
|
|
|
GUIStyle clientStatusStyle = new GUIStyle(EditorStyles.label)
|
|
|
|
|
{
|
|
|
|
|
fontSize = 12,
|
|
|
|
|
fontStyle = FontStyle.Bold
|
|
|
|
|
};
|
|
|
|
|
EditorGUILayout.LabelField(mcpClient.configStatus, clientStatusStyle, GUILayout.Height(28));
|
|
|
|
|
EditorGUILayout.EndHorizontal();
|
2025-08-13 10:04:47 +08:00
|
|
|
// When Claude CLI is missing, show a clear install hint directly below status
|
|
|
|
|
if (mcpClient.mcpType == McpTypes.ClaudeCode && string.IsNullOrEmpty(ExecPath.ResolveClaude()))
|
|
|
|
|
{
|
|
|
|
|
GUIStyle installHintStyle = new GUIStyle(clientStatusStyle);
|
|
|
|
|
installHintStyle.normal.textColor = new Color(1f, 0.5f, 0f); // orange
|
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
|
|
|
GUIContent installText = new GUIContent("Make sure Claude Code is installed!");
|
|
|
|
|
Vector2 textSize = installHintStyle.CalcSize(installText);
|
|
|
|
|
EditorGUILayout.LabelField(installText, installHintStyle, GUILayout.Height(22), GUILayout.Width(textSize.x + 2), GUILayout.ExpandWidth(false));
|
|
|
|
|
GUIStyle helpLinkStyle = new GUIStyle(EditorStyles.linkLabel) { fontStyle = FontStyle.Bold };
|
|
|
|
|
GUILayout.Space(6);
|
|
|
|
|
if (GUILayout.Button("[CLICK]", helpLinkStyle, GUILayout.Height(22), GUILayout.ExpandWidth(false)))
|
|
|
|
|
{
|
|
|
|
|
Application.OpenURL("https://github.com/CoplayDev/unity-mcp/wiki/Troubleshooting-Unity-MCP-and-Claude-Code");
|
|
|
|
|
}
|
|
|
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-13 11:56:22 +08:00
|
|
|
EditorGUILayout.Space(10);
|
|
|
|
|
|
|
|
|
|
// If uv is missing for required clients, show hint and picker then exit early to avoid showing other controls
|
|
|
|
|
if (uvRequired && uvMissingEarly)
|
|
|
|
|
{
|
|
|
|
|
GUIStyle installHintStyle2 = new GUIStyle(EditorStyles.label)
|
|
|
|
|
{
|
|
|
|
|
fontSize = 12,
|
|
|
|
|
fontStyle = FontStyle.Bold,
|
|
|
|
|
wordWrap = false
|
|
|
|
|
};
|
|
|
|
|
installHintStyle2.normal.textColor = new Color(1f, 0.5f, 0f);
|
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
|
|
|
GUIContent installText2 = new GUIContent("Make sure uv is installed!");
|
|
|
|
|
Vector2 sz = installHintStyle2.CalcSize(installText2);
|
|
|
|
|
EditorGUILayout.LabelField(installText2, installHintStyle2, GUILayout.Height(22), GUILayout.Width(sz.x + 2), GUILayout.ExpandWidth(false));
|
|
|
|
|
GUIStyle helpLinkStyle2 = new GUIStyle(EditorStyles.linkLabel) { fontStyle = FontStyle.Bold };
|
|
|
|
|
GUILayout.Space(6);
|
|
|
|
|
if (GUILayout.Button("[CLICK]", helpLinkStyle2, GUILayout.Height(22), GUILayout.ExpandWidth(false)))
|
|
|
|
|
{
|
|
|
|
|
Application.OpenURL("https://github.com/CoplayDev/unity-mcp/wiki/Troubleshooting-Unity-MCP-and-Cursor,-VSCode-&-Windsurf");
|
|
|
|
|
}
|
|
|
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
|
|
|
|
|
|
EditorGUILayout.Space(8);
|
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
|
|
|
if (GUILayout.Button("Choose UV Install Location", GUILayout.Width(260), GUILayout.Height(22)))
|
|
|
|
|
{
|
|
|
|
|
string suggested = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/opt/homebrew/bin" : Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
|
|
|
|
|
string picked = EditorUtility.OpenFilePanel("Select 'uv' binary", suggested, "");
|
|
|
|
|
if (!string.IsNullOrEmpty(picked))
|
|
|
|
|
{
|
|
|
|
|
EditorPrefs.SetString("UnityMCP.UvPath", picked);
|
|
|
|
|
ConfigureMcpClient(mcpClient);
|
|
|
|
|
Repaint();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-07-24 11:31:47 +08:00
|
|
|
|
|
|
|
|
// Action buttons in horizontal layout
|
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
|
|
|
|
2025-05-12 09:25:21 +08:00
|
|
|
if (mcpClient.mcpType == McpTypes.VSCode)
|
2025-03-20 19:24:31 +08:00
|
|
|
{
|
2025-07-24 11:31:47 +08:00
|
|
|
if (GUILayout.Button("Auto Configure", GUILayout.Height(32)))
|
2025-05-12 09:25:21 +08:00
|
|
|
{
|
|
|
|
|
ConfigureMcpClient(mcpClient);
|
|
|
|
|
}
|
2025-07-24 11:31:47 +08:00
|
|
|
}
|
2025-08-13 10:04:47 +08:00
|
|
|
else if (mcpClient.mcpType == McpTypes.ClaudeCode)
|
|
|
|
|
{
|
|
|
|
|
bool claudeAvailable = !string.IsNullOrEmpty(ExecPath.ResolveClaude());
|
|
|
|
|
if (claudeAvailable)
|
|
|
|
|
{
|
|
|
|
|
bool isConfigured = mcpClient.status == McpStatus.Configured;
|
|
|
|
|
string buttonText = isConfigured ? "Unregister UnityMCP with Claude Code" : "Register with Claude Code";
|
|
|
|
|
if (GUILayout.Button(buttonText, GUILayout.Height(32)))
|
|
|
|
|
{
|
|
|
|
|
if (isConfigured)
|
|
|
|
|
{
|
|
|
|
|
UnregisterWithClaudeCode();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
string pythonDir = FindPackagePythonDirectory();
|
|
|
|
|
RegisterWithClaudeCode(pythonDir);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Hide the picker once a valid binary is available
|
|
|
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
|
|
|
GUIStyle pathLabelStyle = new GUIStyle(EditorStyles.miniLabel) { wordWrap = true };
|
|
|
|
|
string resolvedClaude = ExecPath.ResolveClaude();
|
|
|
|
|
EditorGUILayout.LabelField($"Claude CLI: {resolvedClaude}", pathLabelStyle);
|
|
|
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
|
|
|
}
|
|
|
|
|
// CLI picker row (only when not found)
|
|
|
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
|
|
|
if (!claudeAvailable)
|
|
|
|
|
{
|
|
|
|
|
// Only show the picker button in not-found state (no redundant "not found" label)
|
|
|
|
|
if (GUILayout.Button("Choose Claude Install Location", GUILayout.Width(260), GUILayout.Height(22)))
|
|
|
|
|
{
|
|
|
|
|
string suggested = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "/opt/homebrew/bin" : Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
|
|
|
|
|
string picked = EditorUtility.OpenFilePanel("Select 'claude' CLI", suggested, "");
|
|
|
|
|
if (!string.IsNullOrEmpty(picked))
|
|
|
|
|
{
|
|
|
|
|
ExecPath.SetClaudeCliPath(picked);
|
|
|
|
|
// Auto-register after setting a valid path
|
|
|
|
|
string pythonDir = FindPackagePythonDirectory();
|
|
|
|
|
RegisterWithClaudeCode(pythonDir);
|
|
|
|
|
Repaint();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
|
|
|
}
|
2025-07-24 11:31:47 +08:00
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if (GUILayout.Button($"Auto Configure", GUILayout.Height(32)))
|
2025-05-12 09:25:21 +08:00
|
|
|
{
|
2025-07-24 11:31:47 +08:00
|
|
|
ConfigureMcpClient(mcpClient);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-29 01:45:07 +08:00
|
|
|
if (mcpClient.mcpType != McpTypes.ClaudeCode)
|
2025-07-24 11:31:47 +08:00
|
|
|
{
|
2025-07-29 01:45:07 +08:00
|
|
|
if (GUILayout.Button("Manual Setup", GUILayout.Height(32)))
|
2025-07-24 11:31:47 +08:00
|
|
|
{
|
2025-07-29 01:45:07 +08:00
|
|
|
string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
|
|
|
|
? mcpClient.windowsConfigPath
|
|
|
|
|
: mcpClient.linuxConfigPath;
|
|
|
|
|
|
|
|
|
|
if (mcpClient.mcpType == McpTypes.VSCode)
|
2025-05-12 09:25:21 +08:00
|
|
|
{
|
2025-07-29 01:45:07 +08:00
|
|
|
string pythonDir = FindPackagePythonDirectory();
|
2025-07-29 02:55:08 +08:00
|
|
|
string uvPath = FindUvPath();
|
|
|
|
|
if (uvPath == null)
|
|
|
|
|
{
|
|
|
|
|
UnityEngine.Debug.LogError("UV package manager not found. Cannot configure VSCode.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-29 01:45:07 +08:00
|
|
|
var vscodeConfig = new
|
2025-05-12 09:25:21 +08:00
|
|
|
{
|
2025-07-29 01:45:07 +08:00
|
|
|
mcp = new
|
2025-05-12 09:25:21 +08:00
|
|
|
{
|
2025-07-29 01:45:07 +08:00
|
|
|
servers = new
|
2025-05-12 09:25:21 +08:00
|
|
|
{
|
2025-07-29 01:45:07 +08:00
|
|
|
unityMCP = new
|
|
|
|
|
{
|
2025-07-29 02:55:08 +08:00
|
|
|
command = uvPath,
|
2025-07-29 01:45:07 +08:00
|
|
|
args = new[] { "--directory", pythonDir, "run", "server.py" }
|
|
|
|
|
}
|
2025-05-12 09:25:21 +08:00
|
|
|
}
|
|
|
|
|
}
|
2025-07-29 01:45:07 +08:00
|
|
|
};
|
|
|
|
|
JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented };
|
|
|
|
|
string manualConfigJson = JsonConvert.SerializeObject(vscodeConfig, jsonSettings);
|
|
|
|
|
VSCodeManualSetupWindow.ShowWindow(configPath, manualConfigJson);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
ShowManualInstructionsWindow(configPath, mcpClient);
|
|
|
|
|
}
|
2025-05-12 09:25:21 +08:00
|
|
|
}
|
2025-03-20 19:24:31 +08:00
|
|
|
}
|
2025-07-24 11:31:47 +08:00
|
|
|
|
2025-03-20 19:24:31 +08:00
|
|
|
EditorGUILayout.EndHorizontal();
|
2025-07-24 11:31:47 +08:00
|
|
|
|
2025-08-13 10:04:47 +08:00
|
|
|
EditorGUILayout.Space(8);
|
|
|
|
|
// Quick info (hide when Claude is not found to avoid confusion)
|
2025-08-13 11:56:22 +08:00
|
|
|
bool hideConfigInfo =
|
|
|
|
|
(mcpClient.mcpType == McpTypes.ClaudeCode && string.IsNullOrEmpty(ExecPath.ResolveClaude()))
|
|
|
|
|
|| ((mcpClient.mcpType != McpTypes.ClaudeCode) && string.IsNullOrEmpty(FindUvPath()));
|
2025-08-13 10:04:47 +08:00
|
|
|
if (!hideConfigInfo)
|
|
|
|
|
{
|
|
|
|
|
GUIStyle configInfoStyle = new GUIStyle(EditorStyles.miniLabel)
|
|
|
|
|
{
|
|
|
|
|
fontSize = 10
|
|
|
|
|
};
|
|
|
|
|
EditorGUILayout.LabelField($"Config: {Path.GetFileName(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? mcpClient.windowsConfigPath : mcpClient.linuxConfigPath)}", configInfoStyle);
|
|
|
|
|
}
|
2025-03-20 19:24:31 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ToggleUnityBridge()
|
|
|
|
|
{
|
|
|
|
|
if (isUnityBridgeRunning)
|
|
|
|
|
{
|
2025-04-08 18:14:13 +08:00
|
|
|
UnityMcpBridge.Stop();
|
2025-03-20 19:24:31 +08:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2025-04-08 18:14:13 +08:00
|
|
|
UnityMcpBridge.Start();
|
2025-03-20 19:24:31 +08:00
|
|
|
}
|
2025-08-08 06:32:03 +08:00
|
|
|
// Reflect the actual state post-operation (avoid optimistic toggle)
|
|
|
|
|
isUnityBridgeRunning = UnityMcpBridge.IsRunning;
|
|
|
|
|
Repaint();
|
2025-03-20 19:24:31 +08:00
|
|
|
}
|
|
|
|
|
|
2025-05-12 09:25:21 +08:00
|
|
|
private string WriteToConfig(string pythonDir, string configPath, McpClient mcpClient = null)
|
2025-03-20 19:24:31 +08:00
|
|
|
{
|
2025-07-29 02:55:08 +08:00
|
|
|
string uvPath = FindUvPath();
|
|
|
|
|
if (uvPath == null)
|
|
|
|
|
{
|
|
|
|
|
return "UV package manager not found. Please install UV first.";
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-20 19:24:31 +08:00
|
|
|
// Create configuration object for unityMCP
|
2025-04-09 21:10:21 +08:00
|
|
|
McpConfigServer unityMCPConfig = new()
|
2025-03-20 19:24:31 +08:00
|
|
|
{
|
2025-07-29 02:55:08 +08:00
|
|
|
command = uvPath,
|
2025-04-08 18:14:13 +08:00
|
|
|
args = new[] { "--directory", pythonDir, "run", "server.py" },
|
2025-08-13 11:56:22 +08:00
|
|
|
type = "stdio",
|
2025-03-20 19:24:31 +08:00
|
|
|
};
|
|
|
|
|
|
2025-04-09 21:10:21 +08:00
|
|
|
JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented };
|
2025-03-20 19:24:31 +08:00
|
|
|
|
|
|
|
|
// Read existing config if it exists
|
|
|
|
|
string existingJson = "{}";
|
|
|
|
|
if (File.Exists(configPath))
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
existingJson = File.ReadAllText(configPath);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
2025-07-29 01:45:07 +08:00
|
|
|
UnityEngine.Debug.LogWarning($"Error reading existing config: {e.Message}.");
|
2025-03-20 19:24:31 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse the existing JSON while preserving all properties
|
2025-08-13 11:56:22 +08:00
|
|
|
dynamic existingConfig;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(existingJson))
|
|
|
|
|
{
|
|
|
|
|
existingConfig = new Newtonsoft.Json.Linq.JObject();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
existingConfig = JsonConvert.DeserializeObject(existingJson) ?? new Newtonsoft.Json.Linq.JObject();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
// If user has partial/invalid JSON (e.g., mid-edit), start from a fresh object
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(existingJson))
|
|
|
|
|
{
|
|
|
|
|
UnityEngine.Debug.LogWarning("UnityMCP: VSCode mcp.json could not be parsed; rewriting servers block.");
|
|
|
|
|
}
|
|
|
|
|
existingConfig = new Newtonsoft.Json.Linq.JObject();
|
|
|
|
|
}
|
2025-03-20 19:24:31 +08:00
|
|
|
|
2025-05-12 09:25:21 +08:00
|
|
|
// Handle different client types with a switch statement
|
2025-07-24 11:31:47 +08:00
|
|
|
//Comments: Interestingly, VSCode has mcp.servers.unityMCP while others have mcpServers.unityMCP, which is why we need to prevent this
|
2025-05-12 09:25:21 +08:00
|
|
|
switch (mcpClient?.mcpType)
|
2025-03-20 19:24:31 +08:00
|
|
|
{
|
2025-05-12 09:25:21 +08:00
|
|
|
case McpTypes.VSCode:
|
2025-08-13 11:56:22 +08:00
|
|
|
// VSCode-specific configuration (top-level "servers")
|
|
|
|
|
if (existingConfig.servers == null)
|
2025-05-12 09:25:21 +08:00
|
|
|
{
|
2025-08-13 11:56:22 +08:00
|
|
|
existingConfig.servers = new Newtonsoft.Json.Linq.JObject();
|
2025-05-12 09:25:21 +08:00
|
|
|
}
|
2025-07-24 11:31:47 +08:00
|
|
|
|
2025-08-13 11:56:22 +08:00
|
|
|
// Add/update UnityMCP server in VSCode mcp.json
|
|
|
|
|
existingConfig.servers.unityMCP =
|
2025-07-24 11:31:47 +08:00
|
|
|
JsonConvert.DeserializeObject<Newtonsoft.Json.Linq.JToken>(
|
|
|
|
|
JsonConvert.SerializeObject(unityMCPConfig)
|
|
|
|
|
);
|
2025-05-12 09:25:21 +08:00
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
// Standard MCP configuration (Claude Desktop, Cursor, etc.)
|
|
|
|
|
// Ensure mcpServers object exists
|
|
|
|
|
if (existingConfig.mcpServers == null)
|
|
|
|
|
{
|
|
|
|
|
existingConfig.mcpServers = new Newtonsoft.Json.Linq.JObject();
|
|
|
|
|
}
|
2025-07-24 11:31:47 +08:00
|
|
|
|
|
|
|
|
// Add/update UnityMCP server in standard MCP settings
|
|
|
|
|
existingConfig.mcpServers.unityMCP =
|
|
|
|
|
JsonConvert.DeserializeObject<Newtonsoft.Json.Linq.JToken>(
|
|
|
|
|
JsonConvert.SerializeObject(unityMCPConfig)
|
|
|
|
|
);
|
2025-05-12 09:25:21 +08:00
|
|
|
break;
|
2025-03-20 19:24:31 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Write the merged configuration back to file
|
|
|
|
|
string mergedJson = JsonConvert.SerializeObject(existingConfig, jsonSettings);
|
|
|
|
|
File.WriteAllText(configPath, mergedJson);
|
|
|
|
|
|
|
|
|
|
return "Configured successfully";
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-09 05:16:25 +08:00
|
|
|
private void ShowManualConfigurationInstructions(
|
|
|
|
|
string configPath,
|
|
|
|
|
McpClient mcpClient
|
|
|
|
|
)
|
2025-03-20 19:24:31 +08:00
|
|
|
{
|
|
|
|
|
mcpClient.SetStatus(McpStatus.Error, "Manual configuration required");
|
|
|
|
|
|
|
|
|
|
ShowManualInstructionsWindow(configPath, mcpClient);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// New method to show manual instructions without changing status
|
|
|
|
|
private void ShowManualInstructionsWindow(string configPath, McpClient mcpClient)
|
|
|
|
|
{
|
|
|
|
|
// Get the Python directory path using Package Manager API
|
|
|
|
|
string pythonDir = FindPackagePythonDirectory();
|
2025-05-12 09:25:21 +08:00
|
|
|
string manualConfigJson;
|
|
|
|
|
|
2025-05-12 16:05:56 +08:00
|
|
|
// Create common JsonSerializerSettings
|
|
|
|
|
JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented };
|
|
|
|
|
|
|
|
|
|
// Use switch statement to handle different client types
|
|
|
|
|
switch (mcpClient.mcpType)
|
2025-03-20 19:24:31 +08:00
|
|
|
{
|
2025-05-12 16:05:56 +08:00
|
|
|
case McpTypes.VSCode:
|
|
|
|
|
// Create VSCode-specific configuration with proper format
|
|
|
|
|
var vscodeConfig = new
|
2025-03-20 19:24:31 +08:00
|
|
|
{
|
2025-08-13 11:56:22 +08:00
|
|
|
servers = new
|
2025-05-12 09:25:21 +08:00
|
|
|
{
|
2025-08-13 11:56:22 +08:00
|
|
|
unityMCP = new
|
2025-05-12 09:25:21 +08:00
|
|
|
{
|
2025-08-13 11:56:22 +08:00
|
|
|
command = "uv",
|
|
|
|
|
args = new[] { "--directory", pythonDir, "run", "server.py" },
|
|
|
|
|
type = "stdio"
|
2025-05-12 09:25:21 +08:00
|
|
|
}
|
|
|
|
|
}
|
2025-05-12 16:05:56 +08:00
|
|
|
};
|
|
|
|
|
manualConfigJson = JsonConvert.SerializeObject(vscodeConfig, jsonSettings);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
// Create standard MCP configuration for other clients
|
2025-07-29 02:55:08 +08:00
|
|
|
string uvPath = FindUvPath();
|
|
|
|
|
if (uvPath == null)
|
|
|
|
|
{
|
|
|
|
|
UnityEngine.Debug.LogError("UV package manager not found. Cannot configure manual setup.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-12 16:05:56 +08:00
|
|
|
McpConfig jsonConfig = new()
|
2025-05-12 09:25:21 +08:00
|
|
|
{
|
2025-05-12 16:05:56 +08:00
|
|
|
mcpServers = new McpConfigServers
|
2025-05-12 09:25:21 +08:00
|
|
|
{
|
2025-05-12 16:05:56 +08:00
|
|
|
unityMCP = new McpConfigServer
|
|
|
|
|
{
|
2025-07-29 02:55:08 +08:00
|
|
|
command = uvPath,
|
2025-05-12 16:05:56 +08:00
|
|
|
args = new[] { "--directory", pythonDir, "run", "server.py" },
|
|
|
|
|
},
|
2025-05-12 09:25:21 +08:00
|
|
|
},
|
2025-05-12 16:05:56 +08:00
|
|
|
};
|
|
|
|
|
manualConfigJson = JsonConvert.SerializeObject(jsonConfig, jsonSettings);
|
|
|
|
|
break;
|
2025-05-12 09:25:21 +08:00
|
|
|
}
|
2025-03-20 19:24:31 +08:00
|
|
|
|
|
|
|
|
ManualConfigEditorWindow.ShowWindow(configPath, manualConfigJson, mcpClient);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private string FindPackagePythonDirectory()
|
|
|
|
|
{
|
2025-04-09 21:10:21 +08:00
|
|
|
string pythonDir = ServerInstaller.GetServerPath();
|
2025-03-20 19:24:31 +08:00
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
2025-08-08 08:43:33 +08:00
|
|
|
// Only check dev paths if we're using a file-based package (development mode)
|
|
|
|
|
bool isDevelopmentMode = IsDevelopmentMode();
|
|
|
|
|
if (isDevelopmentMode)
|
|
|
|
|
{
|
|
|
|
|
string currentPackagePath = Path.GetDirectoryName(Application.dataPath);
|
|
|
|
|
string[] devPaths = {
|
|
|
|
|
Path.Combine(currentPackagePath, "unity-mcp", "UnityMcpServer", "src"),
|
|
|
|
|
Path.Combine(Path.GetDirectoryName(currentPackagePath), "unity-mcp", "UnityMcpServer", "src"),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
foreach (string devPath in devPaths)
|
|
|
|
|
{
|
|
|
|
|
if (Directory.Exists(devPath) && File.Exists(Path.Combine(devPath, "server.py")))
|
|
|
|
|
{
|
2025-08-09 02:23:45 +08:00
|
|
|
if (debugLogsEnabled)
|
|
|
|
|
{
|
|
|
|
|
UnityEngine.Debug.Log($"Currently in development mode. Package: {devPath}");
|
|
|
|
|
}
|
2025-08-08 08:43:33 +08:00
|
|
|
return devPath;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-12 23:32:51 +08:00
|
|
|
// Resolve via shared helper (handles local registry and older fallback)
|
|
|
|
|
if (ServerPathResolver.TryFindEmbeddedServerSource(out string embedded))
|
2025-04-08 18:14:13 +08:00
|
|
|
{
|
2025-08-12 23:32:51 +08:00
|
|
|
return embedded;
|
2025-03-20 19:24:31 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If still not found, return the placeholder path
|
2025-08-09 02:23:45 +08:00
|
|
|
if (debugLogsEnabled)
|
|
|
|
|
{
|
|
|
|
|
UnityEngine.Debug.LogWarning("Could not find Python directory, using placeholder path");
|
|
|
|
|
}
|
2025-03-20 19:24:31 +08:00
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
2025-07-29 01:45:07 +08:00
|
|
|
UnityEngine.Debug.LogError($"Error finding package path: {e.Message}");
|
2025-03-20 19:24:31 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return pythonDir;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-08 08:43:33 +08:00
|
|
|
private bool IsDevelopmentMode()
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2025-08-08 23:24:50 +08:00
|
|
|
// Only treat as development if manifest explicitly references a local file path for the package
|
2025-08-08 08:43:33 +08:00
|
|
|
string manifestPath = Path.Combine(Application.dataPath, "..", "Packages", "manifest.json");
|
2025-08-08 23:24:50 +08:00
|
|
|
if (!File.Exists(manifestPath)) return false;
|
|
|
|
|
|
|
|
|
|
string manifestContent = File.ReadAllText(manifestPath);
|
|
|
|
|
// Look specifically for our package dependency set to a file: URL
|
|
|
|
|
// This avoids auto-enabling dev mode just because a repo exists elsewhere on disk
|
|
|
|
|
if (manifestContent.IndexOf("\"com.justinpbarnett.unity-mcp\"", StringComparison.OrdinalIgnoreCase) >= 0)
|
2025-08-08 08:43:33 +08:00
|
|
|
{
|
2025-08-08 23:24:50 +08:00
|
|
|
int idx = manifestContent.IndexOf("com.justinpbarnett.unity-mcp", StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
// Crude but effective: check for "file:" in the same line/value
|
|
|
|
|
if (manifestContent.IndexOf("file:", idx, StringComparison.OrdinalIgnoreCase) >= 0
|
|
|
|
|
&& manifestContent.IndexOf("\n", idx, StringComparison.OrdinalIgnoreCase) > manifestContent.IndexOf("file:", idx, StringComparison.OrdinalIgnoreCase))
|
2025-08-08 08:43:33 +08:00
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-20 19:24:31 +08:00
|
|
|
private string ConfigureMcpClient(McpClient mcpClient)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
// Determine the config file path based on OS
|
|
|
|
|
string configPath;
|
|
|
|
|
|
|
|
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
|
|
|
{
|
|
|
|
|
configPath = mcpClient.windowsConfigPath;
|
|
|
|
|
}
|
2025-04-08 18:14:13 +08:00
|
|
|
else if (
|
|
|
|
|
RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
|
|
|
|
|
|| RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
|
|
|
|
|
)
|
2025-03-20 19:24:31 +08:00
|
|
|
{
|
|
|
|
|
configPath = mcpClient.linuxConfigPath;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
return "Unsupported OS";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create directory if it doesn't exist
|
|
|
|
|
Directory.CreateDirectory(Path.GetDirectoryName(configPath));
|
|
|
|
|
|
2025-08-08 08:43:33 +08:00
|
|
|
// Find the server.py file location using the same logic as FindPackagePythonDirectory
|
|
|
|
|
string pythonDir = FindPackagePythonDirectory();
|
2025-03-20 19:24:31 +08:00
|
|
|
|
|
|
|
|
if (pythonDir == null || !File.Exists(Path.Combine(pythonDir, "server.py")))
|
|
|
|
|
{
|
|
|
|
|
ShowManualInstructionsWindow(configPath, mcpClient);
|
|
|
|
|
return "Manual Configuration Required";
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-12 09:25:21 +08:00
|
|
|
string result = WriteToConfig(pythonDir, configPath, mcpClient);
|
2025-03-20 19:24:31 +08:00
|
|
|
|
|
|
|
|
// Update the client status after successful configuration
|
|
|
|
|
if (result == "Configured successfully")
|
|
|
|
|
{
|
|
|
|
|
mcpClient.SetStatus(McpStatus.Configured);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
|
|
|
|
// Determine the config file path based on OS for error message
|
|
|
|
|
string configPath = "";
|
|
|
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
|
|
|
{
|
|
|
|
|
configPath = mcpClient.windowsConfigPath;
|
|
|
|
|
}
|
2025-04-08 18:14:13 +08:00
|
|
|
else if (
|
|
|
|
|
RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
|
|
|
|
|
|| RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
|
|
|
|
|
)
|
2025-03-20 19:24:31 +08:00
|
|
|
{
|
|
|
|
|
configPath = mcpClient.linuxConfigPath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ShowManualInstructionsWindow(configPath, mcpClient);
|
2025-07-29 01:45:07 +08:00
|
|
|
UnityEngine.Debug.LogError(
|
2025-04-08 18:14:13 +08:00
|
|
|
$"Failed to configure {mcpClient.name}: {e.Message}\n{e.StackTrace}"
|
|
|
|
|
);
|
2025-03-20 19:24:31 +08:00
|
|
|
return $"Failed to configure {mcpClient.name}";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-08 18:14:13 +08:00
|
|
|
private void ShowCursorManualConfigurationInstructions(
|
|
|
|
|
string configPath,
|
|
|
|
|
McpClient mcpClient
|
|
|
|
|
)
|
2025-03-20 19:24:31 +08:00
|
|
|
{
|
|
|
|
|
mcpClient.SetStatus(McpStatus.Error, "Manual configuration required");
|
|
|
|
|
|
|
|
|
|
// Get the Python directory path using Package Manager API
|
|
|
|
|
string pythonDir = FindPackagePythonDirectory();
|
|
|
|
|
|
|
|
|
|
// Create the manual configuration message
|
2025-07-29 02:55:08 +08:00
|
|
|
string uvPath = FindUvPath();
|
|
|
|
|
if (uvPath == null)
|
|
|
|
|
{
|
|
|
|
|
UnityEngine.Debug.LogError("UV package manager not found. Cannot configure manual setup.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-09 21:10:21 +08:00
|
|
|
McpConfig jsonConfig = new()
|
2025-03-20 19:24:31 +08:00
|
|
|
{
|
2025-04-08 18:14:13 +08:00
|
|
|
mcpServers = new McpConfigServers
|
2025-03-20 19:24:31 +08:00
|
|
|
{
|
2025-04-08 18:14:13 +08:00
|
|
|
unityMCP = new McpConfigServer
|
2025-03-20 19:24:31 +08:00
|
|
|
{
|
2025-07-29 02:55:08 +08:00
|
|
|
command = uvPath,
|
2025-04-08 18:14:13 +08:00
|
|
|
args = new[] { "--directory", pythonDir, "run", "server.py" },
|
|
|
|
|
},
|
|
|
|
|
},
|
2025-03-20 19:24:31 +08:00
|
|
|
};
|
|
|
|
|
|
2025-04-09 21:10:21 +08:00
|
|
|
JsonSerializerSettings jsonSettings = new() { Formatting = Formatting.Indented };
|
2025-03-20 19:24:31 +08:00
|
|
|
string manualConfigJson = JsonConvert.SerializeObject(jsonConfig, jsonSettings);
|
|
|
|
|
|
|
|
|
|
ManualConfigEditorWindow.ShowWindow(configPath, manualConfigJson, mcpClient);
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-24 11:31:47 +08:00
|
|
|
private void LoadValidationLevelSetting()
|
|
|
|
|
{
|
|
|
|
|
string savedLevel = EditorPrefs.GetString("UnityMCP_ScriptValidationLevel", "standard");
|
|
|
|
|
validationLevelIndex = savedLevel.ToLower() switch
|
|
|
|
|
{
|
|
|
|
|
"basic" => 0,
|
|
|
|
|
"standard" => 1,
|
|
|
|
|
"comprehensive" => 2,
|
|
|
|
|
"strict" => 3,
|
|
|
|
|
_ => 1 // Default to Standard
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void SaveValidationLevelSetting()
|
|
|
|
|
{
|
|
|
|
|
string levelString = validationLevelIndex switch
|
|
|
|
|
{
|
|
|
|
|
0 => "basic",
|
|
|
|
|
1 => "standard",
|
|
|
|
|
2 => "comprehensive",
|
|
|
|
|
3 => "strict",
|
|
|
|
|
_ => "standard"
|
|
|
|
|
};
|
|
|
|
|
EditorPrefs.SetString("UnityMCP_ScriptValidationLevel", levelString);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private string GetValidationLevelDescription(int index)
|
|
|
|
|
{
|
|
|
|
|
return index switch
|
|
|
|
|
{
|
|
|
|
|
0 => "Only basic syntax checks (braces, quotes, comments)",
|
|
|
|
|
1 => "Syntax checks + Unity best practices and warnings",
|
|
|
|
|
2 => "All checks + semantic analysis and performance warnings",
|
|
|
|
|
3 => "Full semantic validation with namespace/type resolution (requires Roslyn)",
|
|
|
|
|
_ => "Standard validation"
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static string GetCurrentValidationLevel()
|
|
|
|
|
{
|
|
|
|
|
string savedLevel = EditorPrefs.GetString("UnityMCP_ScriptValidationLevel", "standard");
|
|
|
|
|
return savedLevel;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-20 19:24:31 +08:00
|
|
|
private void CheckMcpConfiguration(McpClient mcpClient)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2025-07-29 01:45:07 +08:00
|
|
|
// Special handling for Claude Code
|
|
|
|
|
if (mcpClient.mcpType == McpTypes.ClaudeCode)
|
|
|
|
|
{
|
|
|
|
|
CheckClaudeCodeConfiguration(mcpClient);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-20 19:24:31 +08:00
|
|
|
string configPath;
|
|
|
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
|
|
|
{
|
|
|
|
|
configPath = mcpClient.windowsConfigPath;
|
|
|
|
|
}
|
2025-04-08 18:14:13 +08:00
|
|
|
else if (
|
|
|
|
|
RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
|
|
|
|
|
|| RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
|
|
|
|
|
)
|
2025-03-20 19:24:31 +08:00
|
|
|
{
|
|
|
|
|
configPath = mcpClient.linuxConfigPath;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
mcpClient.SetStatus(McpStatus.UnsupportedOS);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!File.Exists(configPath))
|
|
|
|
|
{
|
|
|
|
|
mcpClient.SetStatus(McpStatus.NotConfigured);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string configJson = File.ReadAllText(configPath);
|
2025-08-08 08:43:33 +08:00
|
|
|
// Use the same path resolution as configuration to avoid false "Incorrect Path" in dev mode
|
|
|
|
|
string pythonDir = FindPackagePythonDirectory();
|
2025-05-12 09:25:21 +08:00
|
|
|
|
2025-05-12 16:05:56 +08:00
|
|
|
// Use switch statement to handle different client types, extracting common logic
|
|
|
|
|
string[] args = null;
|
|
|
|
|
bool configExists = false;
|
|
|
|
|
|
2025-05-12 09:25:21 +08:00
|
|
|
switch (mcpClient.mcpType)
|
2025-03-20 19:24:31 +08:00
|
|
|
{
|
2025-05-12 09:25:21 +08:00
|
|
|
case McpTypes.VSCode:
|
|
|
|
|
dynamic config = JsonConvert.DeserializeObject(configJson);
|
|
|
|
|
|
2025-08-13 11:56:22 +08:00
|
|
|
// New schema: top-level servers
|
|
|
|
|
if (config?.servers?.unityMCP != null)
|
|
|
|
|
{
|
|
|
|
|
args = config.servers.unityMCP.args.ToObject<string[]>();
|
|
|
|
|
configExists = true;
|
|
|
|
|
}
|
|
|
|
|
// Back-compat: legacy mcp.servers
|
|
|
|
|
else if (config?.mcp?.servers?.unityMCP != null)
|
2025-05-12 09:25:21 +08:00
|
|
|
{
|
2025-05-12 16:05:56 +08:00
|
|
|
args = config.mcp.servers.unityMCP.args.ToObject<string[]>();
|
|
|
|
|
configExists = true;
|
2025-05-12 09:25:21 +08:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
// Standard MCP configuration check for Claude Desktop, Cursor, etc.
|
|
|
|
|
McpConfig standardConfig = JsonConvert.DeserializeObject<McpConfig>(configJson);
|
2025-05-12 16:05:56 +08:00
|
|
|
|
2025-05-12 09:25:21 +08:00
|
|
|
if (standardConfig?.mcpServers?.unityMCP != null)
|
|
|
|
|
{
|
2025-05-12 16:05:56 +08:00
|
|
|
args = standardConfig.mcpServers.unityMCP.args;
|
|
|
|
|
configExists = true;
|
2025-05-12 09:25:21 +08:00
|
|
|
}
|
|
|
|
|
break;
|
2025-03-20 19:24:31 +08:00
|
|
|
}
|
2025-05-12 16:05:56 +08:00
|
|
|
|
|
|
|
|
// Common logic for checking configuration status
|
|
|
|
|
if (configExists)
|
|
|
|
|
{
|
2025-08-10 06:08:28 +08:00
|
|
|
bool matches = pythonDir != null && Array.Exists(args, arg => arg.Contains(pythonDir, StringComparison.Ordinal));
|
|
|
|
|
if (matches)
|
2025-05-12 16:05:56 +08:00
|
|
|
{
|
|
|
|
|
mcpClient.SetStatus(McpStatus.Configured);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2025-08-10 06:08:28 +08:00
|
|
|
// Attempt auto-rewrite once if the package path changed
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
string rewriteResult = WriteToConfig(pythonDir, configPath, mcpClient);
|
|
|
|
|
if (rewriteResult == "Configured successfully")
|
|
|
|
|
{
|
|
|
|
|
if (debugLogsEnabled)
|
|
|
|
|
{
|
|
|
|
|
UnityEngine.Debug.Log($"UnityMCP: Auto-updated MCP config for '{mcpClient.name}' to new path: {pythonDir}");
|
|
|
|
|
}
|
|
|
|
|
mcpClient.SetStatus(McpStatus.Configured);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
mcpClient.SetStatus(McpStatus.IncorrectPath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
mcpClient.SetStatus(McpStatus.IncorrectPath);
|
|
|
|
|
if (debugLogsEnabled)
|
|
|
|
|
{
|
|
|
|
|
UnityEngine.Debug.LogWarning($"UnityMCP: Auto-config rewrite failed for '{mcpClient.name}': {ex.Message}");
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-05-12 16:05:56 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
mcpClient.SetStatus(McpStatus.MissingConfig);
|
|
|
|
|
}
|
2025-03-20 19:24:31 +08:00
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
|
|
|
|
mcpClient.SetStatus(McpStatus.Error, e.Message);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-29 01:45:07 +08:00
|
|
|
|
|
|
|
|
private void RegisterWithClaudeCode(string pythonDir)
|
|
|
|
|
{
|
2025-08-12 23:32:51 +08:00
|
|
|
// Resolve claude and uv; then run register command
|
|
|
|
|
string claudePath = ExecPath.ResolveClaude();
|
|
|
|
|
if (string.IsNullOrEmpty(claudePath))
|
2025-07-29 01:45:07 +08:00
|
|
|
{
|
2025-08-12 23:32:51 +08:00
|
|
|
UnityEngine.Debug.LogError("UnityMCP: Claude CLI not found. Set a path in this window or install the CLI, then try again.");
|
|
|
|
|
return;
|
2025-07-29 01:45:07 +08:00
|
|
|
}
|
2025-08-12 23:32:51 +08:00
|
|
|
string uvPath = ExecPath.ResolveUv() ?? "uv";
|
2025-07-29 01:45:07 +08:00
|
|
|
|
2025-08-12 23:32:51 +08:00
|
|
|
// Prefer embedded/dev path when available
|
|
|
|
|
string srcDir = !string.IsNullOrEmpty(pythonDirOverride) ? pythonDirOverride : FindPackagePythonDirectory();
|
|
|
|
|
if (string.IsNullOrEmpty(srcDir)) srcDir = pythonDir;
|
2025-07-29 01:45:07 +08:00
|
|
|
|
2025-08-12 23:32:51 +08:00
|
|
|
string args = $"mcp add UnityMCP -- \"{uvPath}\" run --directory \"{srcDir}\" server.py";
|
2025-07-29 01:45:07 +08:00
|
|
|
|
2025-08-12 23:32:51 +08:00
|
|
|
string projectDir = Path.GetDirectoryName(Application.dataPath);
|
2025-08-13 02:56:46 +08:00
|
|
|
// Ensure PATH includes common locations on Unix; on Windows leave PATH as-is
|
|
|
|
|
string pathPrepend = null;
|
|
|
|
|
if (Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.LinuxEditor)
|
|
|
|
|
{
|
|
|
|
|
pathPrepend = Application.platform == RuntimePlatform.OSXEditor
|
|
|
|
|
? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
|
|
|
|
|
: "/usr/local/bin:/usr/bin:/bin";
|
|
|
|
|
}
|
2025-08-12 23:32:51 +08:00
|
|
|
if (!ExecPath.TryRun(claudePath, args, projectDir, out var stdout, out var stderr, 15000, pathPrepend))
|
2025-07-29 01:45:07 +08:00
|
|
|
{
|
2025-08-13 03:14:48 +08:00
|
|
|
string combined = ($"{stdout}\n{stderr}") ?? string.Empty;
|
|
|
|
|
if (combined.IndexOf("already exists", StringComparison.OrdinalIgnoreCase) >= 0)
|
|
|
|
|
{
|
|
|
|
|
// Treat as success if Claude reports existing registration
|
|
|
|
|
var existingClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
|
|
|
|
|
if (existingClient != null) CheckClaudeCodeConfiguration(existingClient);
|
|
|
|
|
Repaint();
|
|
|
|
|
UnityEngine.Debug.Log("<b><color=#2EA3FF>UNITY-MCP</color></b>: UnityMCP already registered with Claude Code.");
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
UnityEngine.Debug.LogError($"UnityMCP: Failed to start Claude CLI.\n{stderr}\n{stdout}");
|
|
|
|
|
}
|
2025-08-12 23:32:51 +08:00
|
|
|
return;
|
2025-07-29 01:45:07 +08:00
|
|
|
}
|
2025-08-12 23:32:51 +08:00
|
|
|
|
|
|
|
|
// Update status
|
|
|
|
|
var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
|
|
|
|
|
if (claudeClient != null) CheckClaudeCodeConfiguration(claudeClient);
|
|
|
|
|
Repaint();
|
|
|
|
|
UnityEngine.Debug.Log("<b><color=#2EA3FF>UNITY-MCP</color></b>: Registered with Claude Code.");
|
2025-07-29 01:45:07 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void UnregisterWithClaudeCode()
|
|
|
|
|
{
|
2025-08-12 23:32:51 +08:00
|
|
|
string claudePath = ExecPath.ResolveClaude();
|
|
|
|
|
if (string.IsNullOrEmpty(claudePath))
|
2025-07-29 01:45:07 +08:00
|
|
|
{
|
2025-08-12 23:32:51 +08:00
|
|
|
UnityEngine.Debug.LogError("UnityMCP: Claude CLI not found. Set a path in this window or install the CLI, then try again.");
|
|
|
|
|
return;
|
2025-07-29 01:45:07 +08:00
|
|
|
}
|
|
|
|
|
|
2025-08-12 23:32:51 +08:00
|
|
|
string projectDir = Path.GetDirectoryName(Application.dataPath);
|
|
|
|
|
string pathPrepend = Application.platform == RuntimePlatform.OSXEditor
|
|
|
|
|
? "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
|
|
|
|
|
: "/usr/local/bin:/usr/bin:/bin";
|
2025-07-29 01:45:07 +08:00
|
|
|
|
2025-08-12 23:32:51 +08:00
|
|
|
if (ExecPath.TryRun(claudePath, "mcp remove UnityMCP", projectDir, out var stdout, out var stderr, 10000, pathPrepend))
|
|
|
|
|
{
|
|
|
|
|
var claudeClient = mcpClients.clients.FirstOrDefault(c => c.mcpType == McpTypes.ClaudeCode);
|
|
|
|
|
if (claudeClient != null)
|
2025-07-29 01:45:07 +08:00
|
|
|
{
|
2025-08-12 23:32:51 +08:00
|
|
|
CheckClaudeCodeConfiguration(claudeClient);
|
2025-07-29 01:45:07 +08:00
|
|
|
}
|
2025-08-12 23:32:51 +08:00
|
|
|
Repaint();
|
|
|
|
|
UnityEngine.Debug.Log("UnityMCP server successfully unregistered from Claude Code.");
|
2025-07-29 01:45:07 +08:00
|
|
|
}
|
2025-08-12 23:32:51 +08:00
|
|
|
else
|
2025-07-29 01:45:07 +08:00
|
|
|
{
|
2025-08-12 23:32:51 +08:00
|
|
|
UnityEngine.Debug.LogWarning($"Claude MCP removal failed: {stderr}\n{stdout}");
|
2025-07-29 01:45:07 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-29 02:55:08 +08:00
|
|
|
private string FindUvPath()
|
|
|
|
|
{
|
|
|
|
|
string uvPath = null;
|
|
|
|
|
|
|
|
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
|
|
|
{
|
|
|
|
|
uvPath = FindWindowsUvPath();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// macOS/Linux paths
|
|
|
|
|
string[] possiblePaths = {
|
|
|
|
|
"/Library/Frameworks/Python.framework/Versions/3.13/bin/uv",
|
|
|
|
|
"/usr/local/bin/uv",
|
|
|
|
|
"/opt/homebrew/bin/uv",
|
|
|
|
|
"/usr/bin/uv"
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
foreach (string path in possiblePaths)
|
|
|
|
|
{
|
2025-07-30 04:01:17 +08:00
|
|
|
if (File.Exists(path) && IsValidUvInstallation(path))
|
2025-07-29 02:55:08 +08:00
|
|
|
{
|
|
|
|
|
uvPath = path;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If not found in common locations, try to find via which command
|
|
|
|
|
if (uvPath == null)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var psi = new ProcessStartInfo
|
|
|
|
|
{
|
|
|
|
|
FileName = "which",
|
|
|
|
|
Arguments = "uv",
|
|
|
|
|
UseShellExecute = false,
|
|
|
|
|
RedirectStandardOutput = true,
|
|
|
|
|
CreateNoWindow = true
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
using var process = Process.Start(psi);
|
|
|
|
|
string output = process.StandardOutput.ReadToEnd().Trim();
|
|
|
|
|
process.WaitForExit();
|
|
|
|
|
|
2025-07-30 04:01:17 +08:00
|
|
|
if (!string.IsNullOrEmpty(output) && File.Exists(output) && IsValidUvInstallation(output))
|
2025-07-29 02:55:08 +08:00
|
|
|
{
|
|
|
|
|
uvPath = output;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
// Ignore errors
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-30 04:01:17 +08:00
|
|
|
// If no specific path found, fall back to using 'uv' from PATH
|
|
|
|
|
if (uvPath == null)
|
|
|
|
|
{
|
|
|
|
|
// Test if 'uv' is available in PATH by trying to run it
|
|
|
|
|
string uvCommand = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "uv.exe" : "uv";
|
|
|
|
|
if (IsValidUvInstallation(uvCommand))
|
|
|
|
|
{
|
|
|
|
|
uvPath = uvCommand;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-29 02:55:08 +08:00
|
|
|
if (uvPath == null)
|
|
|
|
|
{
|
|
|
|
|
UnityEngine.Debug.LogError("UV package manager not found! Please install UV first:\n" +
|
|
|
|
|
"• macOS/Linux: curl -LsSf https://astral.sh/uv/install.sh | sh\n" +
|
|
|
|
|
"• Windows: pip install uv\n" +
|
|
|
|
|
"• Or visit: https://docs.astral.sh/uv/getting-started/installation");
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return uvPath;
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-30 04:01:17 +08:00
|
|
|
private bool IsValidUvInstallation(string uvPath)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var psi = new ProcessStartInfo
|
|
|
|
|
{
|
|
|
|
|
FileName = uvPath,
|
|
|
|
|
Arguments = "--version",
|
|
|
|
|
UseShellExecute = false,
|
|
|
|
|
RedirectStandardOutput = true,
|
|
|
|
|
RedirectStandardError = true,
|
|
|
|
|
CreateNoWindow = true
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
using var process = Process.Start(psi);
|
|
|
|
|
process.WaitForExit(5000); // 5 second timeout
|
|
|
|
|
|
|
|
|
|
if (process.ExitCode == 0)
|
|
|
|
|
{
|
|
|
|
|
string output = process.StandardOutput.ReadToEnd().Trim();
|
|
|
|
|
// Basic validation - just check if it responds with version info
|
|
|
|
|
// UV typically outputs "uv 0.x.x" format
|
|
|
|
|
if (output.StartsWith("uv ") && output.Contains("."))
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-29 01:45:07 +08:00
|
|
|
private string FindWindowsUvPath()
|
|
|
|
|
{
|
|
|
|
|
string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
2025-07-30 04:01:17 +08:00
|
|
|
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
|
|
|
|
string userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
|
|
|
|
|
|
|
|
|
// Dynamic Python version detection - check what's actually installed
|
|
|
|
|
List<string> pythonVersions = new List<string>();
|
|
|
|
|
|
|
|
|
|
// Add common versions but also scan for any Python* directories
|
|
|
|
|
string[] commonVersions = { "Python313", "Python312", "Python311", "Python310", "Python39", "Python38", "Python37" };
|
|
|
|
|
pythonVersions.AddRange(commonVersions);
|
|
|
|
|
|
|
|
|
|
// Scan for additional Python installations
|
|
|
|
|
string[] pythonBasePaths = {
|
|
|
|
|
Path.Combine(appData, "Python"),
|
|
|
|
|
Path.Combine(localAppData, "Programs", "Python"),
|
|
|
|
|
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles) + "\\Python",
|
|
|
|
|
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86) + "\\Python"
|
|
|
|
|
};
|
2025-07-29 01:45:07 +08:00
|
|
|
|
2025-07-30 04:01:17 +08:00
|
|
|
foreach (string basePath in pythonBasePaths)
|
|
|
|
|
{
|
|
|
|
|
if (Directory.Exists(basePath))
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
foreach (string dir in Directory.GetDirectories(basePath, "Python*"))
|
|
|
|
|
{
|
|
|
|
|
string versionName = Path.GetFileName(dir);
|
|
|
|
|
if (!pythonVersions.Contains(versionName))
|
|
|
|
|
{
|
|
|
|
|
pythonVersions.Add(versionName);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
// Ignore directory access errors
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-29 01:45:07 +08:00
|
|
|
|
2025-07-30 04:01:17 +08:00
|
|
|
// Check Python installations for UV
|
2025-07-29 01:45:07 +08:00
|
|
|
foreach (string version in pythonVersions)
|
|
|
|
|
{
|
2025-07-30 04:01:17 +08:00
|
|
|
string[] pythonPaths = {
|
|
|
|
|
Path.Combine(appData, "Python", version, "Scripts", "uv.exe"),
|
|
|
|
|
Path.Combine(localAppData, "Programs", "Python", version, "Scripts", "uv.exe"),
|
|
|
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Python", version, "Scripts", "uv.exe"),
|
|
|
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Python", version, "Scripts", "uv.exe")
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
foreach (string uvPath in pythonPaths)
|
2025-07-29 01:45:07 +08:00
|
|
|
{
|
2025-07-30 04:01:17 +08:00
|
|
|
if (File.Exists(uvPath) && IsValidUvInstallation(uvPath))
|
|
|
|
|
{
|
|
|
|
|
return uvPath;
|
|
|
|
|
}
|
2025-07-29 01:45:07 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-30 04:01:17 +08:00
|
|
|
// Check package manager installations
|
|
|
|
|
string[] packageManagerPaths = {
|
|
|
|
|
// Chocolatey
|
|
|
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "chocolatey", "lib", "uv", "tools", "uv.exe"),
|
|
|
|
|
Path.Combine("C:", "ProgramData", "chocolatey", "lib", "uv", "tools", "uv.exe"),
|
|
|
|
|
|
|
|
|
|
// Scoop
|
|
|
|
|
Path.Combine(userProfile, "scoop", "apps", "uv", "current", "uv.exe"),
|
|
|
|
|
Path.Combine(userProfile, "scoop", "shims", "uv.exe"),
|
|
|
|
|
|
|
|
|
|
// Winget/msstore
|
|
|
|
|
Path.Combine(localAppData, "Microsoft", "WinGet", "Packages", "astral-sh.uv_Microsoft.Winget.Source_8wekyb3d8bbwe", "uv.exe"),
|
|
|
|
|
|
|
|
|
|
// Common standalone installations
|
|
|
|
|
Path.Combine(localAppData, "uv", "uv.exe"),
|
|
|
|
|
Path.Combine(appData, "uv", "uv.exe"),
|
|
|
|
|
Path.Combine(userProfile, ".local", "bin", "uv.exe"),
|
|
|
|
|
Path.Combine(userProfile, "bin", "uv.exe"),
|
|
|
|
|
|
|
|
|
|
// Cargo/Rust installations
|
|
|
|
|
Path.Combine(userProfile, ".cargo", "bin", "uv.exe"),
|
|
|
|
|
|
|
|
|
|
// Manual installations in common locations
|
|
|
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "uv", "uv.exe"),
|
|
|
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "uv", "uv.exe")
|
2025-07-29 01:45:07 +08:00
|
|
|
};
|
|
|
|
|
|
2025-07-30 04:01:17 +08:00
|
|
|
foreach (string uvPath in packageManagerPaths)
|
2025-07-29 01:45:07 +08:00
|
|
|
{
|
2025-07-30 04:01:17 +08:00
|
|
|
if (File.Exists(uvPath) && IsValidUvInstallation(uvPath))
|
|
|
|
|
{
|
|
|
|
|
return uvPath;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try to find uv via where command (Windows equivalent of which)
|
|
|
|
|
// Use where.exe explicitly to avoid PowerShell alias conflicts
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var psi = new ProcessStartInfo
|
|
|
|
|
{
|
|
|
|
|
FileName = "where.exe",
|
|
|
|
|
Arguments = "uv",
|
|
|
|
|
UseShellExecute = false,
|
|
|
|
|
RedirectStandardOutput = true,
|
|
|
|
|
RedirectStandardError = true,
|
|
|
|
|
CreateNoWindow = true
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
using var process = Process.Start(psi);
|
|
|
|
|
string output = process.StandardOutput.ReadToEnd().Trim();
|
|
|
|
|
process.WaitForExit();
|
|
|
|
|
|
|
|
|
|
if (process.ExitCode == 0 && !string.IsNullOrEmpty(output))
|
|
|
|
|
{
|
|
|
|
|
string[] lines = output.Split('\n');
|
|
|
|
|
foreach (string line in lines)
|
|
|
|
|
{
|
|
|
|
|
string cleanPath = line.Trim();
|
|
|
|
|
if (File.Exists(cleanPath) && IsValidUvInstallation(cleanPath))
|
|
|
|
|
{
|
|
|
|
|
return cleanPath;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
// If where.exe fails, try PowerShell's Get-Command as fallback
|
|
|
|
|
try
|
2025-07-29 01:45:07 +08:00
|
|
|
{
|
2025-07-30 04:01:17 +08:00
|
|
|
var psi = new ProcessStartInfo
|
|
|
|
|
{
|
|
|
|
|
FileName = "powershell.exe",
|
|
|
|
|
Arguments = "-Command \"(Get-Command uv -ErrorAction SilentlyContinue).Source\"",
|
|
|
|
|
UseShellExecute = false,
|
|
|
|
|
RedirectStandardOutput = true,
|
|
|
|
|
RedirectStandardError = true,
|
|
|
|
|
CreateNoWindow = true
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
using var process = Process.Start(psi);
|
|
|
|
|
string output = process.StandardOutput.ReadToEnd().Trim();
|
|
|
|
|
process.WaitForExit();
|
|
|
|
|
|
|
|
|
|
if (process.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output))
|
2025-07-29 01:45:07 +08:00
|
|
|
{
|
2025-07-30 04:01:17 +08:00
|
|
|
if (IsValidUvInstallation(output))
|
2025-07-29 01:45:07 +08:00
|
|
|
{
|
2025-07-30 04:01:17 +08:00
|
|
|
return output;
|
2025-07-29 01:45:07 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-30 04:01:17 +08:00
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
// Ignore PowerShell errors too
|
|
|
|
|
}
|
2025-07-29 01:45:07 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null; // Will fallback to using 'uv' from PATH
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-29 06:11:23 +08:00
|
|
|
private string FindClaudeCommand()
|
|
|
|
|
{
|
|
|
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
|
|
|
{
|
|
|
|
|
// Common locations for Claude CLI on Windows
|
|
|
|
|
string[] possiblePaths = {
|
|
|
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "npm", "claude.cmd"),
|
|
|
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "npm", "claude.cmd"),
|
|
|
|
|
"claude.cmd", // Fallback to PATH
|
|
|
|
|
"claude" // Final fallback
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
foreach (string path in possiblePaths)
|
|
|
|
|
{
|
|
|
|
|
if (path.Contains("\\") && File.Exists(path))
|
|
|
|
|
{
|
|
|
|
|
return path;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-30 04:01:17 +08:00
|
|
|
// Try to find via where command (PowerShell compatible)
|
2025-07-29 06:11:23 +08:00
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var psi = new ProcessStartInfo
|
|
|
|
|
{
|
2025-07-30 04:01:17 +08:00
|
|
|
FileName = "where.exe",
|
2025-07-29 06:11:23 +08:00
|
|
|
Arguments = "claude",
|
|
|
|
|
UseShellExecute = false,
|
|
|
|
|
RedirectStandardOutput = true,
|
2025-07-30 04:01:17 +08:00
|
|
|
RedirectStandardError = true,
|
2025-07-29 06:11:23 +08:00
|
|
|
CreateNoWindow = true
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
using var process = Process.Start(psi);
|
|
|
|
|
string output = process.StandardOutput.ReadToEnd().Trim();
|
|
|
|
|
process.WaitForExit();
|
|
|
|
|
|
2025-07-30 04:01:17 +08:00
|
|
|
if (process.ExitCode == 0 && !string.IsNullOrEmpty(output))
|
2025-07-29 06:11:23 +08:00
|
|
|
{
|
|
|
|
|
string[] lines = output.Split('\n');
|
|
|
|
|
foreach (string line in lines)
|
|
|
|
|
{
|
|
|
|
|
string cleanPath = line.Trim();
|
|
|
|
|
if (File.Exists(cleanPath))
|
|
|
|
|
{
|
|
|
|
|
return cleanPath;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
2025-07-30 04:01:17 +08:00
|
|
|
// If where.exe fails, try PowerShell's Get-Command as fallback
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var psi = new ProcessStartInfo
|
|
|
|
|
{
|
|
|
|
|
FileName = "powershell.exe",
|
|
|
|
|
Arguments = "-Command \"(Get-Command claude -ErrorAction SilentlyContinue).Source\"",
|
|
|
|
|
UseShellExecute = false,
|
|
|
|
|
RedirectStandardOutput = true,
|
|
|
|
|
RedirectStandardError = true,
|
|
|
|
|
CreateNoWindow = true
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
using var process = Process.Start(psi);
|
|
|
|
|
string output = process.StandardOutput.ReadToEnd().Trim();
|
|
|
|
|
process.WaitForExit();
|
|
|
|
|
|
|
|
|
|
if (process.ExitCode == 0 && !string.IsNullOrEmpty(output) && File.Exists(output))
|
|
|
|
|
{
|
|
|
|
|
return output;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
// Ignore PowerShell errors too
|
|
|
|
|
}
|
2025-07-29 06:11:23 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "claude"; // Final fallback to PATH
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
return "/usr/local/bin/claude";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-29 01:45:07 +08:00
|
|
|
private void CheckClaudeCodeConfiguration(McpClient mcpClient)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
// Get the Unity project directory to check project-specific config
|
|
|
|
|
string unityProjectDir = Application.dataPath;
|
|
|
|
|
string projectDir = Path.GetDirectoryName(unityProjectDir);
|
|
|
|
|
|
|
|
|
|
// Read the global Claude config file
|
2025-07-30 04:01:17 +08:00
|
|
|
string configPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
|
|
|
|
? mcpClient.windowsConfigPath
|
|
|
|
|
: mcpClient.linuxConfigPath;
|
|
|
|
|
|
2025-08-09 02:23:45 +08:00
|
|
|
if (debugLogsEnabled)
|
|
|
|
|
{
|
|
|
|
|
UnityEngine.Debug.Log($"Checking Claude config at: {configPath}");
|
|
|
|
|
}
|
2025-07-30 04:01:17 +08:00
|
|
|
|
2025-07-29 01:45:07 +08:00
|
|
|
if (!File.Exists(configPath))
|
|
|
|
|
{
|
2025-07-30 04:01:17 +08:00
|
|
|
UnityEngine.Debug.LogWarning($"Claude config file not found at: {configPath}");
|
2025-07-29 01:45:07 +08:00
|
|
|
mcpClient.SetStatus(McpStatus.NotConfigured);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string configJson = File.ReadAllText(configPath);
|
|
|
|
|
dynamic claudeConfig = JsonConvert.DeserializeObject(configJson);
|
|
|
|
|
|
|
|
|
|
// Check for UnityMCP server in the mcpServers section (current format)
|
|
|
|
|
if (claudeConfig?.mcpServers != null)
|
|
|
|
|
{
|
|
|
|
|
var servers = claudeConfig.mcpServers;
|
|
|
|
|
if (servers.UnityMCP != null || servers.unityMCP != null)
|
|
|
|
|
{
|
|
|
|
|
// Found UnityMCP configured
|
|
|
|
|
mcpClient.SetStatus(McpStatus.Configured);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Also check if there's a project-specific configuration for this Unity project (legacy format)
|
|
|
|
|
if (claudeConfig?.projects != null)
|
|
|
|
|
{
|
|
|
|
|
// Look for the project path in the config
|
|
|
|
|
foreach (var project in claudeConfig.projects)
|
|
|
|
|
{
|
|
|
|
|
string projectPath = project.Name;
|
2025-07-30 04:01:17 +08:00
|
|
|
|
|
|
|
|
// Normalize paths for comparison (handle forward/back slash differences)
|
|
|
|
|
string normalizedProjectPath = Path.GetFullPath(projectPath).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
|
|
|
|
string normalizedProjectDir = Path.GetFullPath(projectDir).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
|
|
|
|
|
|
|
|
|
if (string.Equals(normalizedProjectPath, normalizedProjectDir, StringComparison.OrdinalIgnoreCase) && project.Value?.mcpServers != null)
|
2025-07-29 01:45:07 +08:00
|
|
|
{
|
|
|
|
|
// Check for UnityMCP (case variations)
|
|
|
|
|
var servers = project.Value.mcpServers;
|
|
|
|
|
if (servers.UnityMCP != null || servers.unityMCP != null)
|
|
|
|
|
{
|
|
|
|
|
// Found UnityMCP configured for this project
|
|
|
|
|
mcpClient.SetStatus(McpStatus.Configured);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// No configuration found for this project
|
|
|
|
|
mcpClient.SetStatus(McpStatus.NotConfigured);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
{
|
|
|
|
|
UnityEngine.Debug.LogWarning($"Error checking Claude Code config: {e.Message}");
|
|
|
|
|
mcpClient.SetStatus(McpStatus.Error, e.Message);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-09 05:16:25 +08:00
|
|
|
|
|
|
|
|
private bool IsPythonDetected()
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2025-08-10 03:05:47 +08:00
|
|
|
// Windows-specific Python detection
|
|
|
|
|
if (Application.platform == RuntimePlatform.WindowsEditor)
|
2025-08-09 05:16:25 +08:00
|
|
|
{
|
2025-08-10 03:05:47 +08:00
|
|
|
// Common Windows Python installation paths
|
|
|
|
|
string[] windowsCandidates =
|
|
|
|
|
{
|
|
|
|
|
@"C:\Python313\python.exe",
|
|
|
|
|
@"C:\Python312\python.exe",
|
|
|
|
|
@"C:\Python311\python.exe",
|
|
|
|
|
@"C:\Python310\python.exe",
|
|
|
|
|
@"C:\Python39\python.exe",
|
|
|
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python313\python.exe"),
|
|
|
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python312\python.exe"),
|
|
|
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python311\python.exe"),
|
|
|
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python310\python.exe"),
|
|
|
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\Python\Python39\python.exe"),
|
|
|
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python313\python.exe"),
|
|
|
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python312\python.exe"),
|
|
|
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python311\python.exe"),
|
|
|
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python310\python.exe"),
|
|
|
|
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Python39\python.exe"),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
foreach (string c in windowsCandidates)
|
|
|
|
|
{
|
|
|
|
|
if (File.Exists(c)) return true;
|
|
|
|
|
}
|
2025-08-09 05:16:25 +08:00
|
|
|
|
2025-08-10 03:05:47 +08:00
|
|
|
// Try 'where python' command (Windows equivalent of 'which')
|
|
|
|
|
var psi = new ProcessStartInfo
|
|
|
|
|
{
|
|
|
|
|
FileName = "where",
|
|
|
|
|
Arguments = "python",
|
|
|
|
|
UseShellExecute = false,
|
|
|
|
|
RedirectStandardOutput = true,
|
|
|
|
|
RedirectStandardError = true,
|
|
|
|
|
CreateNoWindow = true
|
|
|
|
|
};
|
|
|
|
|
using var p = Process.Start(psi);
|
|
|
|
|
string outp = p.StandardOutput.ReadToEnd().Trim();
|
|
|
|
|
p.WaitForExit(2000);
|
|
|
|
|
if (p.ExitCode == 0 && !string.IsNullOrEmpty(outp))
|
|
|
|
|
{
|
|
|
|
|
string[] lines = outp.Split('\n');
|
|
|
|
|
foreach (string line in lines)
|
|
|
|
|
{
|
|
|
|
|
string trimmed = line.Trim();
|
|
|
|
|
if (File.Exists(trimmed)) return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
2025-08-09 05:16:25 +08:00
|
|
|
{
|
2025-08-10 03:05:47 +08:00
|
|
|
// macOS/Linux detection (existing code)
|
|
|
|
|
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty;
|
|
|
|
|
string[] candidates =
|
|
|
|
|
{
|
|
|
|
|
"/opt/homebrew/bin/python3",
|
|
|
|
|
"/usr/local/bin/python3",
|
|
|
|
|
"/usr/bin/python3",
|
|
|
|
|
"/opt/local/bin/python3",
|
|
|
|
|
Path.Combine(home, ".local", "bin", "python3"),
|
|
|
|
|
"/Library/Frameworks/Python.framework/Versions/3.13/bin/python3",
|
|
|
|
|
"/Library/Frameworks/Python.framework/Versions/3.12/bin/python3",
|
|
|
|
|
};
|
|
|
|
|
foreach (string c in candidates)
|
|
|
|
|
{
|
|
|
|
|
if (File.Exists(c)) return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try 'which python3'
|
|
|
|
|
var psi = new ProcessStartInfo
|
|
|
|
|
{
|
|
|
|
|
FileName = "/usr/bin/which",
|
|
|
|
|
Arguments = "python3",
|
|
|
|
|
UseShellExecute = false,
|
|
|
|
|
RedirectStandardOutput = true,
|
|
|
|
|
RedirectStandardError = true,
|
|
|
|
|
CreateNoWindow = true
|
|
|
|
|
};
|
|
|
|
|
using var p = Process.Start(psi);
|
|
|
|
|
string outp = p.StandardOutput.ReadToEnd().Trim();
|
|
|
|
|
p.WaitForExit(2000);
|
|
|
|
|
if (p.ExitCode == 0 && !string.IsNullOrEmpty(outp) && File.Exists(outp)) return true;
|
|
|
|
|
}
|
2025-08-09 05:16:25 +08:00
|
|
|
}
|
|
|
|
|
catch { }
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2025-03-20 19:24:31 +08:00
|
|
|
}
|
2025-04-08 18:14:13 +08:00
|
|
|
}
|