Files
2026-07-13 12:49:17 +08:00

1109 lines
45 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Services;
using MCPForUnity.Editor.Windows.Components.Advanced;
using MCPForUnity.Editor.Windows.Components.AssetGen;
using MCPForUnity.Editor.Windows.Components.Branding;
using MCPForUnity.Editor.Windows.Components.ClientConfig;
using MCPForUnity.Editor.Windows.Components.Connection;
using MCPForUnity.Editor.Windows.Components.Resources;
using MCPForUnity.Editor.Windows.Components.Tools;
using MCPForUnity.Editor.Setup;
using MCPForUnity.Editor.Windows.Components.Validation;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace MCPForUnity.Editor.Windows
{
public class MCPForUnityEditorWindow : EditorWindow
{
// Section controllers
private McpConnectionSection connectionSection;
private McpClientConfigSection clientConfigSection;
private McpAdvancedSection advancedSection;
private McpToolsSection toolsSection;
private McpResourcesSection resourcesSection;
private McpAssetGenSection assetGenSection;
// UI Elements
private Label versionLabel;
private VisualElement updateNotification;
private Label updateNotificationText;
private ToolbarToggle clientsTabToggle;
private ToolbarToggle depsTabToggle;
private ToolbarToggle advancedTabToggle;
private ToolbarToggle toolsTabToggle;
private ToolbarToggle resourcesTabToggle;
private ToolbarToggle assetGenTabToggle;
private VisualElement clientsPanel;
private VisualElement depsPanel;
private VisualElement advancedPanel;
private VisualElement toolsPanel;
private VisualElement resourcesPanel;
private VisualElement assetGenPanel;
private static readonly HashSet<MCPForUnityEditorWindow> OpenWindows = new();
private bool guiCreated = false;
private bool toolsLoaded = false;
private bool resourcesLoaded = false;
private double lastRefreshTime = 0;
private const double RefreshDebounceSeconds = 0.5;
private bool updateCheckQueued = false;
private bool updateCheckInFlight = false;
private enum ActivePanel
{
Clients,
Deps,
Advanced,
Tools,
Resources,
AssetGen
}
internal static void CloseAllWindows()
{
var windows = OpenWindows.Where(window => window != null).ToArray();
foreach (var window in windows)
{
window.Close();
}
}
public static void ShowWindow()
{
var existingWindows = UnityEngine.Resources.FindObjectsOfTypeAll<MCPForUnityEditorWindow>();
MCPForUnityEditorWindow window = null;
if (existingWindows.Length > 0)
{
window = existingWindows[0];
// If multiple instances exist, keep one and close the extras to avoid stale hidden tabs.
for (int i = 1; i < existingWindows.Length; i++)
{
try
{
existingWindows[i].Close();
}
catch (Exception ex)
{
McpLog.Warn($"Error closing duplicate MCP window: {ex.Message}");
}
}
}
else
{
window = GetWindow<MCPForUnityEditorWindow>(ProductInfo.ProductName);
}
window.titleContent = new GUIContent(ProductInfo.ProductName);
window.minSize = new Vector2(500, 340);
if (window.position.width < 100 || window.position.height < 100)
{
window.position = new Rect(120, 120, 900, 700);
}
window.Show();
window.ShowTab();
window.Focus();
}
// Helper to check and manage open windows from other classes
public static bool HasAnyOpenWindow()
{
return OpenWindows.Count > 0;
}
public static void CloseAllOpenWindows()
{
if (OpenWindows.Count == 0)
return;
// Copy to array to avoid modifying the collection while iterating
var arr = new MCPForUnityEditorWindow[OpenWindows.Count];
OpenWindows.CopyTo(arr);
foreach (var window in arr)
{
try
{
window?.Close();
}
catch (Exception ex)
{
McpLog.Warn($"Error closing MCP window: {ex.Message}");
}
}
}
public void CreateGUI()
{
// Guard against repeated CreateGUI calls (e.g., domain reloads)
if (guiCreated)
return;
string basePath = AssetPathUtility.GetMcpPackageRootPath();
// Load main window UXML
var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
$"{basePath}/Editor/Windows/MCPForUnityEditorWindow.uxml"
);
if (visualTree == null)
{
McpLog.Error(
$"Failed to load UXML at: {basePath}/Editor/Windows/MCPForUnityEditorWindow.uxml"
);
return;
}
rootVisualElement.Clear();
visualTree.CloneTree(rootVisualElement);
// Load main window USS
var mainStyleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>(
$"{basePath}/Editor/Windows/MCPForUnityEditorWindow.uss"
);
if (mainStyleSheet != null)
{
rootVisualElement.styleSheets.Add(mainStyleSheet);
}
// Load common USS
var commonStyleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>(
$"{basePath}/Editor/Windows/Components/Common.uss"
);
if (commonStyleSheet != null)
{
rootVisualElement.styleSheets.Add(commonStyleSheet);
}
// Embed the Ocean brand mark at the left of the header bar
var headerLeft = rootVisualElement.Q<VisualElement>("header-left");
if (headerLeft != null && headerLeft.Q<OceanMark>() == null)
{
var logo = new OceanMark { name = "header-logo" };
logo.AddToClassList("header-logo");
headerLeft.Insert(0, logo);
}
// Cache UI elements
versionLabel = rootVisualElement.Q<Label>("version-label");
updateNotification = rootVisualElement.Q<VisualElement>("update-notification");
updateNotificationText = rootVisualElement.Q<Label>("update-notification-text");
clientsPanel = rootVisualElement.Q<VisualElement>("clients-panel");
depsPanel = rootVisualElement.Q<VisualElement>("deps-panel");
advancedPanel = rootVisualElement.Q<VisualElement>("advanced-panel");
toolsPanel = rootVisualElement.Q<VisualElement>("tools-panel");
resourcesPanel = rootVisualElement.Q<VisualElement>("resources-panel");
assetGenPanel = rootVisualElement.Q<VisualElement>("assetgen-panel");
var clientsContainer = rootVisualElement.Q<VisualElement>("clients-container");
var depsContainer = rootVisualElement.Q<VisualElement>("deps-container");
var advancedContainer = rootVisualElement.Q<VisualElement>("advanced-container");
var toolsContainer = rootVisualElement.Q<VisualElement>("tools-container");
var resourcesContainer = rootVisualElement.Q<VisualElement>("resources-container");
var assetGenContainer = rootVisualElement.Q<VisualElement>("assetgen-container");
if (clientsPanel == null || depsPanel == null || advancedPanel == null || toolsPanel == null || resourcesPanel == null || assetGenPanel == null)
{
McpLog.Error("Failed to find tab panels in UXML");
return;
}
if (clientsContainer == null)
{
McpLog.Error("Failed to find clients-container in UXML");
return;
}
if (depsContainer == null)
{
McpLog.Error("Failed to find deps-container in UXML");
return;
}
if (advancedContainer == null)
{
McpLog.Error("Failed to find advanced-container in UXML");
return;
}
if (toolsContainer == null)
{
McpLog.Error("Failed to find tools-container in UXML");
return;
}
if (resourcesContainer == null)
{
McpLog.Error("Failed to find resources-container in UXML");
return;
}
if (assetGenContainer == null)
{
McpLog.Error("Failed to find assetgen-container in UXML");
return;
}
// Initialize version label
UpdateVersionLabel();
SetupTabs();
// Load and initialize Connection section
var connectionTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
$"{basePath}/Editor/Windows/Components/Connection/McpConnectionSection.uxml"
);
if (connectionTree != null)
{
var connectionRoot = connectionTree.Instantiate();
clientsContainer.Add(connectionRoot);
connectionSection = new McpConnectionSection(connectionRoot);
connectionSection.OnManualConfigUpdateRequested += () =>
clientConfigSection?.UpdateManualConfiguration();
connectionSection.OnTransportChanged += () =>
clientConfigSection?.RefreshSelectedClient(forceImmediate: true);
}
// Load and initialize Client Configuration section
var clientConfigTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
$"{basePath}/Editor/Windows/Components/ClientConfig/McpClientConfigSection.uxml"
);
if (clientConfigTree != null)
{
var clientConfigRoot = clientConfigTree.Instantiate();
clientsContainer.Add(clientConfigRoot);
clientConfigSection = new McpClientConfigSection(clientConfigRoot);
// Wire up transport mismatch detection: when client status is checked,
// update the connection section's warning banner if there's a mismatch
clientConfigSection.OnClientTransportDetected += (clientName, transport) =>
connectionSection?.UpdateTransportMismatchWarning(clientName, transport);
// Wire up version mismatch detection: when client status is checked,
// update the connection section's warning banner if there's a version mismatch
clientConfigSection.OnClientConfigMismatch += (clientName, mismatchMessage) =>
connectionSection?.UpdateVersionMismatchWarning(clientName, mismatchMessage);
}
// Build Dependencies section (replaces old Roslyn + Validation in Deps tab)
BuildDependenciesSection(depsContainer);
// Load and initialize Advanced section
var advancedTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
$"{basePath}/Editor/Windows/Components/Advanced/McpAdvancedSection.uxml"
);
if (advancedTree != null)
{
var advancedRoot = advancedTree.Instantiate();
advancedContainer.Add(advancedRoot);
advancedSection = new McpAdvancedSection(advancedRoot);
// Wire up events from Advanced section
advancedSection.OnGitUrlChanged += () =>
clientConfigSection?.UpdateManualConfiguration();
advancedSection.OnHttpServerCommandUpdateRequested += () =>
{
connectionSection?.UpdateHttpServerCommandDisplay();
connectionSection?.UpdateConnectionStatus();
};
advancedSection.OnTestConnectionRequested += async () =>
{
if (connectionSection != null)
await connectionSection.VerifyBridgeConnectionAsync();
};
advancedSection.OnPackageDeployed += () =>
{
UpdateVersionLabel();
QueueUpdateCheck();
};
// Wire up health status updates from Connection to Advanced
connectionSection?.SetHealthStatusUpdateCallback((isHealthy, statusText) =>
advancedSection?.UpdateHealthStatus(isHealthy, statusText));
}
// Load Validation section into Advanced tab
var validationTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
$"{basePath}/Editor/Windows/Components/Validation/McpValidationSection.uxml"
);
if (validationTree != null)
{
var validationRoot = validationTree.Instantiate();
advancedContainer.Add(validationRoot);
new McpValidationSection(validationRoot);
}
// Load and initialize Tools section
var toolsTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
$"{basePath}/Editor/Windows/Components/Tools/McpToolsSection.uxml"
);
if (toolsTree != null)
{
var toolsRoot = toolsTree.Instantiate();
toolsContainer.Add(toolsRoot);
toolsSection = new McpToolsSection(toolsRoot);
if (toolsTabToggle != null && toolsTabToggle.value)
{
EnsureToolsLoaded();
}
}
else
{
McpLog.Warn("Failed to load tools section UXML. Tool configuration will be unavailable.");
}
// Load and initialize Resources section
var resourcesTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
$"{basePath}/Editor/Windows/Components/Resources/McpResourcesSection.uxml"
);
if (resourcesTree != null)
{
var resourcesRoot = resourcesTree.Instantiate();
resourcesContainer.Add(resourcesRoot);
resourcesSection = new McpResourcesSection(resourcesRoot);
if (resourcesTabToggle != null && resourcesTabToggle.value)
{
EnsureResourcesLoaded();
}
}
else
{
McpLog.Warn("Failed to load resources section UXML. Resource configuration will be unavailable.");
}
// Load and initialize Asset Generation section
var assetGenTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
$"{basePath}/Editor/Windows/Components/AssetGen/McpAssetGenSection.uxml"
);
if (assetGenTree != null)
{
var assetGenRoot = assetGenTree.Instantiate();
assetGenContainer.Add(assetGenRoot);
assetGenSection = new McpAssetGenSection(assetGenRoot);
}
else
{
McpLog.Warn("Failed to load asset generation section UXML. Asset generation configuration will be unavailable.");
}
// Apply .section-last class to last section in each stack
// (Unity UI Toolkit doesn't support :last-child pseudo-class)
ApplySectionLastClasses();
guiCreated = true;
// Initial updates
RefreshAllData();
QueueUpdateCheck();
}
private void UpdateVersionLabel()
{
if (versionLabel == null)
{
return;
}
string version = AssetPathUtility.GetPackageVersion();
versionLabel.text = $"v{version}";
versionLabel.tooltip = AssetPathUtility.IsPreReleaseVersion()
? $"{ProductInfo.ProductName} v{version} (pre-release package, using prerelease server channel)"
: $"{ProductInfo.ProductName} v{version}";
}
private void QueueUpdateCheck()
{
if (updateCheckQueued || updateCheckInFlight)
{
return;
}
updateCheckQueued = true;
EditorApplication.delayCall += CheckForPackageUpdates;
}
private void CheckForPackageUpdates()
{
updateCheckQueued = false;
if (updateNotification == null || updateNotificationText == null)
{
return;
}
string currentVersion = AssetPathUtility.GetPackageVersion();
if (string.IsNullOrEmpty(currentVersion) || currentVersion == "unknown")
{
updateNotification.RemoveFromClassList("visible");
return;
}
// Main thread: resolve service + read EditorPrefs cache (both require main thread)
var updateService = MCPServiceLocator.Updates;
var cachedResult = updateService.TryGetCachedResult(currentVersion);
if (cachedResult != null)
{
ApplyUpdateCheckResult(cachedResult, currentVersion);
return;
}
// Main thread: pre-compute installation info (uses main-thread-only Unity APIs)
bool isGitInstallation = updateService.IsGitInstallation();
string gitBranch = isGitInstallation ? updateService.GetGitUpdateBranch(currentVersion) : "main";
// Background thread: network I/O only (no EditorPrefs or Unity API access)
updateCheckInFlight = true;
Task.Run(() =>
{
try
{
return updateService.FetchAndCompare(currentVersion, isGitInstallation, gitBranch);
}
catch (Exception ex)
{
McpLog.Info($"Package update check skipped: {ex.Message}");
return null;
}
}).ContinueWith(t =>
{
EditorApplication.delayCall += () =>
{
updateCheckInFlight = false;
// Main thread: cache the result in EditorPrefs
var result = t.Status == TaskStatus.RanToCompletion ? t.Result : null;
if (result != null && result.CheckSucceeded && !string.IsNullOrEmpty(result.LatestVersion))
{
updateService.CacheFetchResult(currentVersion, result.LatestVersion);
}
if (this == null || updateNotification == null || updateNotificationText == null)
{
return;
}
ApplyUpdateCheckResult(result, currentVersion);
};
}, TaskScheduler.Default);
}
private void ApplyUpdateCheckResult(UpdateCheckResult result, string currentVersion)
{
if (result != null && result.CheckSucceeded && result.UpdateAvailable && !string.IsNullOrEmpty(result.LatestVersion))
{
updateNotificationText.text = $"Update available: v{result.LatestVersion} (current: v{currentVersion})";
updateNotificationText.tooltip = $"Latest version: v{result.LatestVersion}\nCurrent version: v{currentVersion}";
updateNotification.AddToClassList("visible");
}
else
{
updateNotification.RemoveFromClassList("visible");
}
}
private void EnsureToolsLoaded()
{
if (toolsLoaded)
{
return;
}
if (toolsSection == null)
{
return;
}
toolsLoaded = true;
toolsSection.Refresh();
}
private void EnsureResourcesLoaded()
{
if (resourcesLoaded)
{
return;
}
if (resourcesSection == null)
{
return;
}
resourcesLoaded = true;
resourcesSection.Refresh();
}
/// <summary>
/// Applies the .section-last class to the last .section element in each .section-stack container.
/// This is a workaround for Unity UI Toolkit not supporting the :last-child pseudo-class.
/// </summary>
private void ApplySectionLastClasses()
{
var sectionStacks = rootVisualElement.Query<VisualElement>(className: "section-stack").ToList();
foreach (var stack in sectionStacks)
{
var sections = stack.Children().Where(c => c.ClassListContains("section")).ToList();
if (sections.Count > 0)
{
// Remove class from all sections first (in case of refresh)
foreach (var section in sections)
{
section.RemoveFromClassList("section-last");
}
// Add class to the last section
sections[sections.Count - 1].AddToClassList("section-last");
}
}
}
// Throttle OnEditorUpdate to avoid per-frame overhead (GitHub issue #577).
// Connection status polling every frame caused expensive network checks 60+ times/sec.
private double _lastEditorUpdateTime;
private const double EditorUpdateIntervalSeconds = 2.0;
private void OnEnable()
{
EditorApplication.update += OnEditorUpdate;
OpenWindows.Add(this);
}
private void OnDisable()
{
EditorApplication.update -= OnEditorUpdate;
OpenWindows.Remove(this);
guiCreated = false;
toolsLoaded = false;
resourcesLoaded = false;
}
private void OnFocus()
{
// Only refresh data if UI is built
if (rootVisualElement == null || rootVisualElement.childCount == 0)
return;
RefreshAllData();
}
private void OnEditorUpdate()
{
// Throttle to 2-second intervals instead of every frame.
// This prevents the expensive IsLocalHttpServerReachable() socket checks from running
// 60+ times per second, which caused main thread blocking and GC pressure.
double now = EditorApplication.timeSinceStartup;
if (now - _lastEditorUpdateTime < EditorUpdateIntervalSeconds)
{
return;
}
_lastEditorUpdateTime = now;
if (rootVisualElement == null || rootVisualElement.childCount == 0)
return;
connectionSection?.UpdateConnectionStatus();
}
private void RefreshAllData()
{
// Debounce rapid successive calls (e.g., from OnFocus being called multiple times)
double currentTime = EditorApplication.timeSinceStartup;
if (currentTime - lastRefreshTime < RefreshDebounceSeconds)
{
return;
}
lastRefreshTime = currentTime;
connectionSection?.UpdateConnectionStatus();
if (MCPServiceLocator.Bridge.IsRunning)
{
_ = connectionSection?.VerifyBridgeConnectionAsync();
}
advancedSection?.UpdatePathOverrides();
clientConfigSection?.RefreshSelectedClient();
}
private void SetupTabs()
{
clientsTabToggle = rootVisualElement.Q<ToolbarToggle>("clients-tab");
depsTabToggle = rootVisualElement.Q<ToolbarToggle>("deps-tab");
advancedTabToggle = rootVisualElement.Q<ToolbarToggle>("advanced-tab");
toolsTabToggle = rootVisualElement.Q<ToolbarToggle>("tools-tab");
resourcesTabToggle = rootVisualElement.Q<ToolbarToggle>("resources-tab");
assetGenTabToggle = rootVisualElement.Q<ToolbarToggle>("assetgen-tab");
clientsPanel?.RemoveFromClassList("hidden");
depsPanel?.RemoveFromClassList("hidden");
advancedPanel?.RemoveFromClassList("hidden");
toolsPanel?.RemoveFromClassList("hidden");
resourcesPanel?.RemoveFromClassList("hidden");
assetGenPanel?.RemoveFromClassList("hidden");
if (clientsTabToggle != null)
{
clientsTabToggle.RegisterValueChangedCallback(evt =>
{
if (evt.newValue) SwitchPanel(ActivePanel.Clients);
});
}
if (depsTabToggle != null)
{
depsTabToggle.RegisterValueChangedCallback(evt =>
{
if (evt.newValue) SwitchPanel(ActivePanel.Deps);
});
}
if (advancedTabToggle != null)
{
advancedTabToggle.RegisterValueChangedCallback(evt =>
{
if (evt.newValue) SwitchPanel(ActivePanel.Advanced);
});
}
if (toolsTabToggle != null)
{
toolsTabToggle.RegisterValueChangedCallback(evt =>
{
if (evt.newValue) SwitchPanel(ActivePanel.Tools);
});
}
if (resourcesTabToggle != null)
{
resourcesTabToggle.RegisterValueChangedCallback(evt =>
{
if (evt.newValue) SwitchPanel(ActivePanel.Resources);
});
}
if (assetGenTabToggle != null)
{
assetGenTabToggle.RegisterValueChangedCallback(evt =>
{
if (evt.newValue) SwitchPanel(ActivePanel.AssetGen);
});
}
var savedPanel = EditorPrefs.GetString(EditorPrefKeys.EditorWindowActivePanel, ActivePanel.Clients.ToString());
// Migrate old "Validation" saved value to "Deps"
if (savedPanel == "Validation") savedPanel = "Deps";
if (!Enum.TryParse(savedPanel, out ActivePanel initialPanel))
{
initialPanel = ActivePanel.Clients;
}
SwitchPanel(initialPanel);
}
private void SwitchPanel(ActivePanel panel)
{
// Hide all panels
if (clientsPanel != null)
{
clientsPanel.style.display = DisplayStyle.None;
}
if (depsPanel != null)
{
depsPanel.style.display = DisplayStyle.None;
}
if (advancedPanel != null)
{
advancedPanel.style.display = DisplayStyle.None;
}
if (toolsPanel != null)
{
toolsPanel.style.display = DisplayStyle.None;
}
if (resourcesPanel != null)
{
resourcesPanel.style.display = DisplayStyle.None;
}
if (assetGenPanel != null)
{
assetGenPanel.style.display = DisplayStyle.None;
}
// Show selected panel
switch (panel)
{
case ActivePanel.Clients:
if (clientsPanel != null) clientsPanel.style.display = DisplayStyle.Flex;
// Refresh client status when switching to Connect tab (e.g., after package/version changes).
clientConfigSection?.RefreshSelectedClient(forceImmediate: true);
break;
case ActivePanel.Deps:
if (depsPanel != null) depsPanel.style.display = DisplayStyle.Flex;
break;
case ActivePanel.Advanced:
if (advancedPanel != null) advancedPanel.style.display = DisplayStyle.Flex;
break;
case ActivePanel.Tools:
if (toolsPanel != null) toolsPanel.style.display = DisplayStyle.Flex;
EnsureToolsLoaded();
break;
case ActivePanel.Resources:
if (resourcesPanel != null) resourcesPanel.style.display = DisplayStyle.Flex;
EnsureResourcesLoaded();
break;
case ActivePanel.AssetGen:
if (assetGenPanel != null) assetGenPanel.style.display = DisplayStyle.Flex;
assetGenSection?.Refresh();
break;
}
// Update toggle states
clientsTabToggle?.SetValueWithoutNotify(panel == ActivePanel.Clients);
depsTabToggle?.SetValueWithoutNotify(panel == ActivePanel.Deps);
advancedTabToggle?.SetValueWithoutNotify(panel == ActivePanel.Advanced);
toolsTabToggle?.SetValueWithoutNotify(panel == ActivePanel.Tools);
resourcesTabToggle?.SetValueWithoutNotify(panel == ActivePanel.Resources);
assetGenTabToggle?.SetValueWithoutNotify(panel == ActivePanel.AssetGen);
EditorPrefs.SetString(EditorPrefKeys.EditorWindowActivePanel, panel.ToString());
}
internal static void RequestHealthVerification()
{
foreach (var window in OpenWindows)
{
window?.ScheduleHealthCheck();
}
}
private void ScheduleHealthCheck()
{
EditorApplication.delayCall += async () =>
{
// Ensure window and components are still valid before execution
if (this == null || connectionSection == null)
{
return;
}
try
{
await connectionSection.VerifyBridgeConnectionAsync();
}
catch (Exception ex)
{
// Log but don't crash if verification fails during cleanup
McpLog.Warn($"Health check verification failed: {ex.Message}");
}
};
}
private static void BuildDependenciesSection(VisualElement container)
{
var section = new VisualElement();
section.AddToClassList("section");
var title = new Label("Optional Dependencies");
title.AddToClassList("section-title");
section.Add(title);
var content = new VisualElement();
content.AddToClassList("section-content");
var desc = new Label("Some tool groups require optional packages. Install them to unlock additional capabilities.");
desc.AddToClassList("validation-description");
desc.style.marginBottom = 4;
content.Add(desc);
// Install All / Uninstall All buttons
var bulkRow = new VisualElement();
bulkRow.style.flexDirection = FlexDirection.Row;
bulkRow.style.marginBottom = 8;
var upmPackages = new[] { "com.unity.probuilder", "com.unity.cinemachine", "com.unity.visualeffectgraph", "com.unity.cloud.gltfast" };
Button installAllButton = null;
installAllButton = new Button(() =>
{
if (!EditorUtility.DisplayDialog("Install All Dependencies",
"This will install Roslyn DLLs, ProBuilder, Cinemachine, VFX Graph, and glTFast. Continue?",
"Install All", "Cancel")) return;
installAllButton.SetEnabled(false);
installAllButton.text = "Installing...";
if (!RoslynInstaller.IsInstalled()) RoslynInstaller.Install(interactive: false);
BatchUpmAdd(upmPackages, () =>
{
installAllButton.SetEnabled(true);
installAllButton.text = "Install All";
});
});
installAllButton.text = "Install All";
installAllButton.AddToClassList("action-button");
installAllButton.style.marginRight = 4;
bulkRow.Add(installAllButton);
Button uninstallAllButton = null;
uninstallAllButton = new Button(() =>
{
if (!EditorUtility.DisplayDialog("Uninstall All Dependencies",
"This will remove Roslyn DLLs, ProBuilder, Cinemachine, VFX Graph, and glTFast. Continue?",
"Uninstall All", "Cancel")) return;
uninstallAllButton.SetEnabled(false);
uninstallAllButton.text = "Removing...";
UninstallRoslyn();
BatchUpmRemove(upmPackages, () =>
{
uninstallAllButton.SetEnabled(true);
uninstallAllButton.text = "Uninstall All";
});
});
uninstallAllButton.text = "Uninstall All";
uninstallAllButton.AddToClassList("action-button");
bulkRow.Add(uninstallAllButton);
content.Add(bulkRow);
// Roslyn — for execute_code modern C# support
// Check if Roslyn types are actually loaded (covers NuGet, Plugins folder, etc.)
bool roslynLoaded = Type.GetType("Microsoft.CodeAnalysis.CSharp.CSharpCompilation, Microsoft.CodeAnalysis.CSharp") != null;
bool roslynInstalledLocally = RoslynInstaller.IsInstalled();
AddDependencyRow(content,
"Roslyn (C# 12+ Compiler)",
"Enables modern C# syntax in execute_code tool (scripting_ext group).",
roslynLoaded,
roslynInstalledLocally
? "Installed via Plugins/Roslyn \u2014 execute_code uses Roslyn"
: "Available (loaded from NuGet/external) \u2014 execute_code uses Roslyn",
"Not installed \u2014 execute_code falls back to C# 6 (CodeDom)",
done => { RoslynInstaller.Install(interactive: true); done?.Invoke(); },
roslynInstalledLocally
? (Action<Action>)(done => { UninstallRoslyn(); done?.Invoke(); })
: null);
// ProBuilder
bool hasProBuilder = Type.GetType("UnityEngine.ProBuilder.ProBuilderMesh, Unity.ProBuilder") != null;
AddDependencyRow(content,
"ProBuilder",
"Required for the manage_probuilder tool (probuilder group).",
hasProBuilder,
"Installed",
"Not installed",
done => InstallUpmPackage("com.unity.probuilder", done),
done => RemoveUpmPackage("com.unity.probuilder", done));
// Cinemachine
bool hasCinemachine = Type.GetType("Unity.Cinemachine.CinemachineCamera, Unity.Cinemachine") != null
|| Type.GetType("Cinemachine.CinemachineVirtualCamera, Cinemachine") != null;
AddDependencyRow(content,
"Cinemachine",
"Enhances manage_camera with virtual camera support (core group).",
hasCinemachine,
"Installed",
"Not installed \u2014 camera tool works without it",
done => InstallUpmPackage("com.unity.cinemachine", done),
done => RemoveUpmPackage("com.unity.cinemachine", done));
// VFX Graph — uses preprocessor symbol, so check via UPM package list
bool hasVfxGraph = IsUpmPackageInstalled("com.unity.visualeffectgraph");
AddDependencyRow(content,
"VFX Graph",
"Enables VisualEffect support in manage_vfx tool (vfx group).",
hasVfxGraph,
"Installed",
"Not installed \u2014 VFX tool falls back to ParticleSystem/LineRenderer",
done => InstallUpmPackage("com.unity.visualeffectgraph", done),
done => RemoveUpmPackage("com.unity.visualeffectgraph", done));
// glTFast — uses an assembly type, but also check via UPM package list
bool hasGltfast = IsUpmPackageInstalled("com.unity.cloud.gltfast") || Type.GetType("GLTFast.GltfImport, glTFast") != null;
AddDependencyRow(content,
"glTFast (glTF/GLB import)",
"Enables .glb/.gltf model import for the AI Asset Generation tools (asset_gen group).",
hasGltfast,
"Installed — GLB generation/import works",
"Not installed — GLB import is unavailable; FBX still works, or install to enable GLB",
done => InstallUpmPackage("com.unity.cloud.gltfast", done),
done => RemoveUpmPackage("com.unity.cloud.gltfast", done));
section.Add(content);
container.Add(section);
}
private static void AddDependencyRow(VisualElement parent, string name, string description,
bool isInstalled, string installedText, string missingText,
Action<Action> installAction, Action<Action> uninstallAction)
{
var row = new VisualElement();
row.style.marginBottom = 8;
row.style.paddingBottom = 8;
row.style.borderBottomWidth = 1;
row.style.borderBottomColor = new Color(0.3f, 0.3f, 0.3f, 0.3f);
var header = new VisualElement();
header.style.flexDirection = FlexDirection.Row;
header.style.alignItems = Align.Center;
header.style.marginBottom = 2;
var nameLabel = new Label(name);
nameLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
nameLabel.style.flexGrow = 1;
header.Add(nameLabel);
var statusIcon = new Label(isInstalled ? "\u2713" : "\u2717");
statusIcon.style.color = isInstalled ? new Color(0.4f, 0.8f, 0.4f) : new Color(0.8f, 0.4f, 0.4f);
statusIcon.style.fontSize = 14;
header.Add(statusIcon);
row.Add(header);
var descLabel = new Label(description);
descLabel.AddToClassList("validation-description");
descLabel.style.marginBottom = 2;
row.Add(descLabel);
var statusText = new Label(isInstalled ? installedText : missingText);
statusText.style.fontSize = 11;
statusText.style.color = isInstalled ? new Color(0.6f, 0.8f, 0.6f) : new Color(0.8f, 0.7f, 0.5f);
row.Add(statusText);
var buttonRow = new VisualElement();
buttonRow.style.flexDirection = FlexDirection.Row;
buttonRow.style.marginTop = 4;
if (!isInstalled && installAction != null)
{
Button btn = null;
btn = new Button(() =>
{
btn.SetEnabled(false);
btn.text = "Installing...";
Action restore = () =>
{
btn.SetEnabled(true);
btn.text = "Install";
};
try { installAction(restore); }
catch (Exception e)
{
Debug.LogError($"[MCP] Install failed: {e.Message}");
restore();
}
});
btn.text = "Install";
btn.AddToClassList("action-button");
buttonRow.Add(btn);
}
if (isInstalled && uninstallAction != null)
{
Button btn = null;
btn = new Button(() =>
{
if (!EditorUtility.DisplayDialog("Remove " + name,
$"Are you sure you want to remove {name}?", "Remove", "Cancel")) return;
btn.SetEnabled(false);
btn.text = "Removing...";
Action restore = () =>
{
btn.SetEnabled(true);
btn.text = "Uninstall";
};
try { uninstallAction(restore); }
catch (Exception e)
{
Debug.LogError($"[MCP] Uninstall failed: {e.Message}");
restore();
}
});
btn.text = "Uninstall";
btn.AddToClassList("action-button");
buttonRow.Add(btn);
}
if (buttonRow.childCount > 0)
row.Add(buttonRow);
parent.Add(row);
}
private static void InstallUpmPackage(string packageId, Action onComplete = null)
{
BatchUpmAdd(new[] { packageId }, onComplete);
}
private static void RemoveUpmPackage(string packageId, Action onComplete = null)
{
BatchUpmRemove(new[] { packageId }, onComplete);
}
private static void BatchUpmAdd(string[] packageIds, Action onComplete = null)
{
var request = UnityEditor.PackageManager.Client.AddAndRemove(packageIds, null);
EditorUtility.DisplayProgressBar("Installing Packages", $"Installing {packageIds.Length} package(s)...", 0.5f);
PollUpmRequest(request, "install", onComplete);
}
private static void BatchUpmRemove(string[] packageIds, Action onComplete = null)
{
var request = UnityEditor.PackageManager.Client.AddAndRemove(null, packageIds);
EditorUtility.DisplayProgressBar("Removing Packages", $"Removing {packageIds.Length} package(s)...", 0.5f);
PollUpmRequest(request, "remove", onComplete);
}
private static void PollUpmRequest(UnityEditor.PackageManager.Requests.AddAndRemoveRequest request, string verb, Action onComplete)
{
EditorApplication.CallbackFunction pollCallback = null;
pollCallback = () =>
{
if (!request.IsCompleted) return;
EditorApplication.update -= pollCallback;
EditorUtility.ClearProgressBar();
if (request.Status == UnityEditor.PackageManager.StatusCode.Success)
Debug.Log($"[MCP] Package {verb} succeeded.");
else
Debug.LogError($"[MCP] Package {verb} failed: {request.Error?.message}");
onComplete?.Invoke();
};
EditorApplication.update += pollCallback;
}
private static void UninstallRoslyn()
{
string folder = System.IO.Path.Combine(Application.dataPath, "Plugins/Roslyn");
if (System.IO.Directory.Exists(folder))
{
System.IO.Directory.Delete(folder, true);
string metaPath = folder + ".meta";
if (System.IO.File.Exists(metaPath))
System.IO.File.Delete(metaPath);
AssetDatabase.Refresh();
Debug.Log("[MCP] Roslyn DLLs removed from Assets/Plugins/Roslyn/");
}
}
private static bool IsUpmPackageInstalled(string packageId)
{
// Check manifest.json directly — faster than async UPM API
string manifestPath = System.IO.Path.Combine(Application.dataPath, "../Packages/manifest.json");
if (!System.IO.File.Exists(manifestPath)) return false;
string manifest = System.IO.File.ReadAllText(manifestPath);
return manifest.Contains($"\"{packageId}\"");
}
}
}