unity-mcp/UnityMcpBridge/Editor/Setup/SetupWizardWindow.cs

727 lines
26 KiB
C#
Raw Normal View History

feat: Unity Asset Store compliance with post-installation dependency setup (#281) * feat: implement Unity Asset Store compliance with post-installation dependency setup - Remove bundled Python dependencies from Unity package - Add comprehensive 6-step setup wizard with auto-trigger on first import - Implement cross-platform dependency detection (Windows, macOS, Linux) - Add integrated MCP client configuration within setup process - Create production-ready menu structure with clean UI/UX - Ensure complete end-to-end setup requiring no additional configuration - Add comprehensive error handling and recovery mechanisms This implementation ensures Asset Store compliance while maintaining full functionality through guided user setup. Users are left 100% ready to use MCP after completing the setup wizard. * refactor: improve Asset Store compliance implementation with production-ready setup - Remove automatic installation attempts on package import - Always show setup wizard on package install/reinstall - Integrate MCP client configuration as part of setup wizard process - Ensure MCP client config window remains accessible via menu - Remove testing components for production readiness - Replace automatic installation with manual guidance only - Add complete 4-step setup flow: Welcome → Dependencies → Installation Guide → Client Configuration → Complete - Improve user experience with clear instructions and accessible client management * feat: add comprehensive dependency requirement warnings - Add critical warnings throughout setup wizard that package cannot function without dependencies - Update package.json description to clearly state manual dependency installation requirement - Prevent setup completion if dependencies are missing - Enhance skip setup warning to emphasize package will be non-functional - Add error messages explaining consequences of missing dependencies - Update menu item to indicate setup wizard is required - Ensure users understand package is completely non-functional without proper dependency installation * refactor: simplify setup wizard for production BREAKING: Reduced setup wizard from 5 steps to 3 streamlined steps: - Step 1: Setup (welcome + dependency check + installation guide) - Step 2: Configure (client configuration with direct access to full settings) - Step 3: Complete (final status and quick access to resources) Simplifications: - Consolidated UI components with DRY helper methods (DrawSectionTitle, DrawSuccessStatus, DrawErrorStatus) - Simplified dependency status display with clean icons and essential info - Removed complex state management - using simple EditorPrefs instead - Removed unused InstallationOrchestrator and SetupState classes - Streamlined client configuration to direct users to full settings window - Simplified navigation with back/skip/next buttons - Reduced code complexity while maintaining solid principles Results: - 40% less code while maintaining all functionality - Cleaner, more intuitive user flow - Faster setup process with fewer clicks - Production-ready simplicity - Easier maintenance and debugging * fix: add missing using statement for DependencyCheckResult Add missing 'using MCPForUnity.Editor.Dependencies.Models;' to resolve DependencyCheckResult type reference in SetupWizard.cs * refactor: optimize dependency checks and remove dead code * fix: remove unused DrawInstallationProgressStep method Removes leftover method that references deleted _isInstalling and _installationStatus fields, fixing compilation errors. * feat: improve setup wizard UX and add real client configuration 1. Remove dependency mentions from package.json description 2. Only show dependency warnings when dependencies are actually missing 3. Add actual MCP client configuration functionality within the wizard: - Client selection dropdown - Individual client configuration - Claude Code registration/unregistration - Batch configuration for all clients - Manual setup instructions - Real configuration file writing Users can now complete full setup including client configuration without leaving the wizard. * refactor: improve menu text and client restart tip - Remove '(Required)' from Setup Wizard menu item for cleaner appearance - Update tip to reflect that most AI clients auto-detect configuration changes * refactor: simplify client restart tip message * fix: add missing using statement for MCPForUnityEditorWindow Add 'using MCPForUnity.Editor.Windows;' to resolve unresolved symbol error for MCPForUnityEditorWindow in SetupWizard.cs * Format code * Remove unused folders * Same for validators * Same for Setup... * feat: add setup wizard persistence to avoid showing on subsequent imports * fix: update Python version check to support Python 4+ across all platform detectors * refactor: extract common platform detection logic into PlatformDetectorBase class * feat: add configuration helpers for MCP client setup with sophisticated path resolution * fix: add missing override keyword to DetectPython method in platform detectors * fix: update menu item labels for consistent capitalization and naming * fix: standardize "MCP For Unity" capitalization in window titles and dialogs * refactor: update package ID from justinpbarnett to coplaydev across codebase * refactor: remove unused validation and configuration helper methods * refactor: remove unused warnOnLegacyPackageId parameter from TryFindEmbeddedServerSource --------- Co-authored-by: Claude <claude@anthropic.com> Co-authored-by: Marcus Sanatan <msanatan@gmail.com>
2025-10-04 04:43:40 +08:00
using System;
using System.Linq;
using MCPForUnity.Editor.Data;
using MCPForUnity.Editor.Dependencies;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Models;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Setup
{
/// <summary>
/// Setup wizard window for guiding users through dependency installation
/// </summary>
public class SetupWizardWindow : EditorWindow
{
private DependencyCheckResult _dependencyResult;
private Vector2 _scrollPosition;
private int _currentStep = 0;
private McpClients _mcpClients;
private int _selectedClientIndex = 0;
private readonly string[] _stepTitles = {
"Setup",
"Configure",
"Complete"
};
public static void ShowWindow(DependencyCheckResult dependencyResult = null)
{
var window = GetWindow<SetupWizardWindow>("MCP for Unity Setup");
window.minSize = new Vector2(500, 400);
window.maxSize = new Vector2(800, 600);
window._dependencyResult = dependencyResult ?? DependencyManager.CheckAllDependencies();
window.Show();
}
private void OnEnable()
{
if (_dependencyResult == null)
{
_dependencyResult = DependencyManager.CheckAllDependencies();
}
_mcpClients = new McpClients();
// Check client configurations on startup
foreach (var client in _mcpClients.clients)
{
CheckClientConfiguration(client);
}
}
private void OnGUI()
{
DrawHeader();
DrawProgressBar();
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
switch (_currentStep)
{
case 0: DrawSetupStep(); break;
case 1: DrawConfigureStep(); break;
case 2: DrawCompleteStep(); break;
}
EditorGUILayout.EndScrollView();
DrawFooter();
}
private void DrawHeader()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
GUILayout.Label("MCP for Unity Setup Wizard", EditorStyles.boldLabel);
GUILayout.FlexibleSpace();
GUILayout.Label($"Step {_currentStep + 1} of {_stepTitles.Length}");
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
// Step title
var titleStyle = new GUIStyle(EditorStyles.largeLabel)
{
fontSize = 16,
fontStyle = FontStyle.Bold
};
EditorGUILayout.LabelField(_stepTitles[_currentStep], titleStyle);
EditorGUILayout.Space();
}
private void DrawProgressBar()
{
var rect = EditorGUILayout.GetControlRect(false, 4);
var progress = (_currentStep + 1) / (float)_stepTitles.Length;
EditorGUI.ProgressBar(rect, progress, "");
EditorGUILayout.Space();
}
private void DrawSetupStep()
{
// Welcome section
DrawSectionTitle("MCP for Unity Setup");
EditorGUILayout.LabelField(
"This wizard will help you set up MCP for Unity to connect AI assistants with your Unity Editor.",
EditorStyles.wordWrappedLabel
);
EditorGUILayout.Space();
// Dependency check section
EditorGUILayout.BeginHorizontal();
DrawSectionTitle("System Check", 14);
GUILayout.FlexibleSpace();
if (GUILayout.Button("Refresh", GUILayout.Width(60), GUILayout.Height(20)))
{
_dependencyResult = DependencyManager.CheckAllDependencies();
}
EditorGUILayout.EndHorizontal();
// Show simplified dependency status
foreach (var dep in _dependencyResult.Dependencies)
{
DrawSimpleDependencyStatus(dep);
}
// Overall status and installation guidance
EditorGUILayout.Space();
if (!_dependencyResult.IsSystemReady)
{
// Only show critical warnings when dependencies are actually missing
EditorGUILayout.HelpBox(
"⚠️ Missing Dependencies: MCP for Unity requires Python 3.10+ and UV package manager to function properly.",
MessageType.Warning
);
EditorGUILayout.Space();
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
DrawErrorStatus("Installation Required");
var recommendations = DependencyManager.GetInstallationRecommendations();
EditorGUILayout.LabelField(recommendations, EditorStyles.wordWrappedLabel);
EditorGUILayout.Space();
if (GUILayout.Button("Open Installation Links", GUILayout.Height(25)))
{
OpenInstallationUrls();
}
EditorGUILayout.EndVertical();
}
else
{
DrawSuccessStatus("System Ready");
EditorGUILayout.LabelField("All requirements are met. You can proceed to configure your AI clients.", EditorStyles.wordWrappedLabel);
}
}
private void DrawCompleteStep()
{
DrawSectionTitle("Setup Complete");
// Refresh dependency check with caching to avoid heavy operations on every repaint
if (_dependencyResult == null || (DateTime.UtcNow - _dependencyResult.CheckedAt).TotalSeconds > 2)
{
_dependencyResult = DependencyManager.CheckAllDependencies();
}
if (_dependencyResult.IsSystemReady)
{
DrawSuccessStatus("MCP for Unity Ready!");
EditorGUILayout.HelpBox(
"🎉 MCP for Unity is now set up and ready to use!\n\n" +
"• Dependencies verified\n" +
"• MCP server ready\n" +
"• Client configuration accessible",
MessageType.Info
);
EditorGUILayout.Space();
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Documentation", GUILayout.Height(30)))
{
Application.OpenURL("https://github.com/CoplayDev/unity-mcp");
}
if (GUILayout.Button("Client Settings", GUILayout.Height(30)))
{
Windows.MCPForUnityEditorWindow.ShowWindow();
}
EditorGUILayout.EndHorizontal();
}
else
{
DrawErrorStatus("Setup Incomplete - Package Non-Functional");
EditorGUILayout.HelpBox(
"🚨 MCP for Unity CANNOT work - dependencies still missing!\n\n" +
"Install ALL required dependencies before the package will function.",
MessageType.Error
);
var missingDeps = _dependencyResult.GetMissingRequired();
if (missingDeps.Count > 0)
{
EditorGUILayout.Space();
EditorGUILayout.LabelField("Still Missing:", EditorStyles.boldLabel);
foreach (var dep in missingDeps)
{
EditorGUILayout.LabelField($"✗ {dep.Name}", EditorStyles.label);
}
}
EditorGUILayout.Space();
if (GUILayout.Button("Go Back to Setup", GUILayout.Height(30)))
{
_currentStep = 0;
}
}
}
// Helper methods for consistent UI components
private void DrawSectionTitle(string title, int fontSize = 16)
{
var titleStyle = new GUIStyle(EditorStyles.boldLabel)
{
fontSize = fontSize,
fontStyle = FontStyle.Bold
};
EditorGUILayout.LabelField(title, titleStyle);
EditorGUILayout.Space();
}
private void DrawSuccessStatus(string message)
{
var originalColor = GUI.color;
GUI.color = Color.green;
EditorGUILayout.LabelField($"✓ {message}", EditorStyles.boldLabel);
GUI.color = originalColor;
EditorGUILayout.Space();
}
private void DrawErrorStatus(string message)
{
var originalColor = GUI.color;
GUI.color = Color.red;
EditorGUILayout.LabelField($"✗ {message}", EditorStyles.boldLabel);
GUI.color = originalColor;
EditorGUILayout.Space();
}
private void DrawSimpleDependencyStatus(DependencyStatus dep)
{
EditorGUILayout.BeginHorizontal();
var statusIcon = dep.IsAvailable ? "✓" : "✗";
var statusColor = dep.IsAvailable ? Color.green : Color.red;
var originalColor = GUI.color;
GUI.color = statusColor;
GUILayout.Label(statusIcon, GUILayout.Width(20));
EditorGUILayout.LabelField(dep.Name, EditorStyles.boldLabel);
GUI.color = originalColor;
if (!dep.IsAvailable && !string.IsNullOrEmpty(dep.ErrorMessage))
{
EditorGUILayout.LabelField($"({dep.ErrorMessage})", EditorStyles.miniLabel);
}
EditorGUILayout.EndHorizontal();
}
private void DrawConfigureStep()
{
DrawSectionTitle("AI Client Configuration");
// Check dependencies first (with caching to avoid heavy operations on every repaint)
if (_dependencyResult == null || (DateTime.UtcNow - _dependencyResult.CheckedAt).TotalSeconds > 2)
{
_dependencyResult = DependencyManager.CheckAllDependencies();
}
if (!_dependencyResult.IsSystemReady)
{
DrawErrorStatus("Cannot Configure - System Requirements Not Met");
EditorGUILayout.HelpBox(
"Client configuration requires system dependencies to be installed first. Please complete setup before proceeding.",
MessageType.Warning
);
if (GUILayout.Button("Go Back to Setup", GUILayout.Height(30)))
{
_currentStep = 0;
}
return;
}
EditorGUILayout.LabelField(
"Configure your AI assistants to work with Unity. Select a client below to set it up:",
EditorStyles.wordWrappedLabel
);
EditorGUILayout.Space();
// Client selection and configuration
if (_mcpClients.clients.Count > 0)
{
// Client selector dropdown
string[] clientNames = _mcpClients.clients.Select(c => c.name).ToArray();
EditorGUI.BeginChangeCheck();
_selectedClientIndex = EditorGUILayout.Popup("Select AI Client:", _selectedClientIndex, clientNames);
if (EditorGUI.EndChangeCheck())
{
_selectedClientIndex = Mathf.Clamp(_selectedClientIndex, 0, _mcpClients.clients.Count - 1);
// Refresh client status when selection changes
CheckClientConfiguration(_mcpClients.clients[_selectedClientIndex]);
}
EditorGUILayout.Space();
var selectedClient = _mcpClients.clients[_selectedClientIndex];
DrawClientConfigurationInWizard(selectedClient);
EditorGUILayout.Space();
// Batch configuration option
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField("Quick Setup", EditorStyles.boldLabel);
EditorGUILayout.LabelField(
"Automatically configure all detected AI clients at once:",
EditorStyles.wordWrappedLabel
);
EditorGUILayout.Space();
if (GUILayout.Button("Configure All Detected Clients", GUILayout.Height(30)))
{
ConfigureAllClientsInWizard();
}
EditorGUILayout.EndVertical();
}
else
{
EditorGUILayout.HelpBox("No AI clients detected. Make sure you have Claude Code, Cursor, or VSCode installed.", MessageType.Info);
}
EditorGUILayout.Space();
EditorGUILayout.HelpBox(
"💡 You might need to restart your AI client after configuring.",
MessageType.Info
);
}
private void DrawFooter()
{
EditorGUILayout.Space();
EditorGUILayout.BeginHorizontal();
// Back button
GUI.enabled = _currentStep > 0;
if (GUILayout.Button("Back", GUILayout.Width(60)))
{
_currentStep--;
}
GUILayout.FlexibleSpace();
// Skip button
if (GUILayout.Button("Skip", GUILayout.Width(60)))
{
bool dismiss = EditorUtility.DisplayDialog(
"Skip Setup",
"⚠️ Skipping setup will leave MCP for Unity non-functional!\n\n" +
"You can restart setup from: Window > MCP for Unity > Setup Wizard (Required)",
"Skip Anyway",
"Cancel"
);
if (dismiss)
{
SetupWizard.MarkSetupDismissed();
Close();
}
}
// Next/Done button
GUI.enabled = true;
string buttonText = _currentStep == _stepTitles.Length - 1 ? "Done" : "Next";
if (GUILayout.Button(buttonText, GUILayout.Width(80)))
{
if (_currentStep == _stepTitles.Length - 1)
{
SetupWizard.MarkSetupCompleted();
Close();
}
else
{
_currentStep++;
}
}
GUI.enabled = true;
EditorGUILayout.EndHorizontal();
}
private void DrawClientConfigurationInWizard(McpClient client)
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
EditorGUILayout.LabelField($"{client.name} Configuration", EditorStyles.boldLabel);
EditorGUILayout.Space();
// Show current status
var statusColor = GetClientStatusColor(client);
var originalColor = GUI.color;
GUI.color = statusColor;
EditorGUILayout.LabelField($"Status: {client.configStatus}", EditorStyles.label);
GUI.color = originalColor;
EditorGUILayout.Space();
// Configuration buttons
EditorGUILayout.BeginHorizontal();
if (client.mcpType == McpTypes.ClaudeCode)
{
// Special handling for Claude Code
bool claudeAvailable = !string.IsNullOrEmpty(ExecPath.ResolveClaude());
if (claudeAvailable)
{
bool isConfigured = client.status == McpStatus.Configured;
string buttonText = isConfigured ? "Unregister" : "Register";
if (GUILayout.Button($"{buttonText} with Claude Code"))
{
if (isConfigured)
{
UnregisterFromClaudeCode(client);
}
else
{
RegisterWithClaudeCode(client);
}
}
}
else
{
EditorGUILayout.HelpBox("Claude Code not found. Please install Claude Code first.", MessageType.Warning);
if (GUILayout.Button("Open Claude Code Website"))
{
Application.OpenURL("https://claude.ai/download");
}
}
}
else
{
// Standard client configuration
if (GUILayout.Button($"Configure {client.name}"))
{
ConfigureClientInWizard(client);
}
if (GUILayout.Button("Manual Setup"))
{
ShowManualSetupInWizard(client);
}
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
}
private Color GetClientStatusColor(McpClient client)
{
return client.status switch
{
McpStatus.Configured => Color.green,
McpStatus.Running => Color.green,
McpStatus.Connected => Color.green,
McpStatus.IncorrectPath => Color.yellow,
McpStatus.CommunicationError => Color.yellow,
McpStatus.NoResponse => Color.yellow,
_ => Color.red
};
}
private void ConfigureClientInWizard(McpClient client)
{
try
{
string result = PerformClientConfiguration(client);
EditorUtility.DisplayDialog(
$"{client.name} Configuration",
result,
"OK"
);
// Refresh client status
CheckClientConfiguration(client);
Repaint();
}
catch (System.Exception ex)
{
EditorUtility.DisplayDialog(
"Configuration Error",
$"Failed to configure {client.name}: {ex.Message}",
"OK"
);
}
}
private void ConfigureAllClientsInWizard()
{
int successCount = 0;
int totalCount = _mcpClients.clients.Count;
foreach (var client in _mcpClients.clients)
{
try
{
if (client.mcpType == McpTypes.ClaudeCode)
{
if (!string.IsNullOrEmpty(ExecPath.ResolveClaude()) && client.status != McpStatus.Configured)
{
RegisterWithClaudeCode(client);
successCount++;
}
else if (client.status == McpStatus.Configured)
{
successCount++; // Already configured
}
}
else
{
string result = PerformClientConfiguration(client);
if (result.Contains("success", System.StringComparison.OrdinalIgnoreCase))
{
successCount++;
}
}
CheckClientConfiguration(client);
}
catch (System.Exception ex)
{
McpLog.Error($"Failed to configure {client.name}: {ex.Message}");
}
}
EditorUtility.DisplayDialog(
"Batch Configuration Complete",
$"Successfully configured {successCount} out of {totalCount} clients.\n\n" +
"Restart your AI clients for changes to take effect.",
"OK"
);
Repaint();
}
private void RegisterWithClaudeCode(McpClient client)
{
try
{
string pythonDir = McpPathResolver.FindPackagePythonDirectory();
string claudePath = ExecPath.ResolveClaude();
string uvPath = ExecPath.ResolveUv() ?? "uv";
string args = $"mcp add UnityMCP -- \"{uvPath}\" run --directory \"{pythonDir}\" server.py";
if (!ExecPath.TryRun(claudePath, args, null, out var stdout, out var stderr, 15000, McpPathResolver.GetPathPrepend()))
{
if ((stdout + stderr).Contains("already exists", System.StringComparison.OrdinalIgnoreCase))
{
CheckClientConfiguration(client);
EditorUtility.DisplayDialog("Claude Code", "MCP for Unity is already registered with Claude Code.", "OK");
}
else
{
throw new System.Exception($"Registration failed: {stderr}");
}
}
else
{
CheckClientConfiguration(client);
EditorUtility.DisplayDialog("Claude Code", "Successfully registered MCP for Unity with Claude Code!", "OK");
}
}
catch (System.Exception ex)
{
EditorUtility.DisplayDialog("Registration Error", $"Failed to register with Claude Code: {ex.Message}", "OK");
}
}
private void UnregisterFromClaudeCode(McpClient client)
{
try
{
string claudePath = ExecPath.ResolveClaude();
if (ExecPath.TryRun(claudePath, "mcp remove UnityMCP", null, out var stdout, out var stderr, 10000, McpPathResolver.GetPathPrepend()))
{
CheckClientConfiguration(client);
EditorUtility.DisplayDialog("Claude Code", "Successfully unregistered MCP for Unity from Claude Code.", "OK");
}
else
{
throw new System.Exception($"Unregistration failed: {stderr}");
}
}
catch (System.Exception ex)
{
EditorUtility.DisplayDialog("Unregistration Error", $"Failed to unregister from Claude Code: {ex.Message}", "OK");
}
}
private string PerformClientConfiguration(McpClient client)
{
// This mirrors the logic from MCPForUnityEditorWindow.ConfigureMcpClient
string configPath = McpConfigurationHelper.GetClientConfigPath(client);
string pythonDir = McpPathResolver.FindPackagePythonDirectory();
if (string.IsNullOrEmpty(pythonDir))
{
return "Manual configuration required - Python server directory not found.";
}
McpConfigurationHelper.EnsureConfigDirectoryExists(configPath);
return McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client);
}
private void ShowManualSetupInWizard(McpClient client)
{
string configPath = McpConfigurationHelper.GetClientConfigPath(client);
string pythonDir = McpPathResolver.FindPackagePythonDirectory();
string uvPath = ServerInstaller.FindUvPath();
if (string.IsNullOrEmpty(uvPath))
{
EditorUtility.DisplayDialog("Manual Setup", "UV package manager not found. Please install UV first.", "OK");
return;
}
// Build manual configuration using the sophisticated helper logic
string result = McpConfigurationHelper.WriteMcpConfiguration(pythonDir, configPath, client);
string manualConfig;
if (result == "Configured successfully")
{
// Read back the configuration that was written
try
{
manualConfig = System.IO.File.ReadAllText(configPath);
}
catch
{
manualConfig = "Configuration written successfully, but could not read back for display.";
}
}
else
{
manualConfig = $"Configuration failed: {result}";
}
EditorUtility.DisplayDialog(
$"Manual Setup - {client.name}",
$"Configuration file location:\n{configPath}\n\n" +
$"Configuration result:\n{manualConfig}",
"OK"
);
}
private void CheckClientConfiguration(McpClient client)
{
// Basic status check - could be enhanced to mirror MCPForUnityEditorWindow logic
try
{
string configPath = McpConfigurationHelper.GetClientConfigPath(client);
if (System.IO.File.Exists(configPath))
{
client.configStatus = "Configured";
client.status = McpStatus.Configured;
}
else
{
client.configStatus = "Not Configured";
client.status = McpStatus.NotConfigured;
}
}
catch
{
client.configStatus = "Error";
client.status = McpStatus.Error;
}
}
private void OpenInstallationUrls()
{
var (pythonUrl, uvUrl) = DependencyManager.GetInstallationUrls();
bool openPython = EditorUtility.DisplayDialog(
"Open Installation URLs",
"Open Python installation page?",
"Yes",
"No"
);
if (openPython)
{
Application.OpenURL(pythonUrl);
}
bool openUV = EditorUtility.DisplayDialog(
"Open Installation URLs",
"Open UV installation page?",
"Yes",
"No"
);
if (openUV)
{
Application.OpenURL(uvUrl);
}
}
}
}