升级XR插件版本

This commit is contained in:
Sora丶kong
2026-03-02 17:56:21 +08:00
parent 8962657674
commit 60f512a9bc
1317 changed files with 110305 additions and 48249 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 82074be914aefa84cb557c599d2319b3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7723ed5eaaccb104e93acb9fd2d8cd32
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,467 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Services;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace MCPForUnity.Editor.Windows.Components.Advanced
{
/// <summary>
/// Controller for the Advanced Settings section.
/// Handles path overrides, server source configuration, dev mode, and package deployment.
/// </summary>
public class McpAdvancedSection
{
// UI Elements
private TextField uvxPathOverride;
private Button browseUvxButton;
private Button clearUvxButton;
private VisualElement uvxPathStatus;
private TextField gitUrlOverride;
private Button browseGitUrlButton;
private Button clearGitUrlButton;
private Toggle debugLogsToggle;
private Toggle devModeForceRefreshToggle;
private Toggle useBetaServerToggle;
private TextField deploySourcePath;
private Button browseDeploySourceButton;
private Button clearDeploySourceButton;
private Button deployButton;
private Button deployRestoreButton;
private Label deployTargetLabel;
private Label deployBackupLabel;
private Label deployStatusLabel;
private VisualElement healthIndicator;
private Label healthStatus;
private Button testConnectionButton;
// Events
public event Action OnGitUrlChanged;
public event Action OnHttpServerCommandUpdateRequested;
public event Action OnTestConnectionRequested;
public event Action<bool> OnBetaModeChanged;
public VisualElement Root { get; private set; }
public McpAdvancedSection(VisualElement root)
{
Root = root;
CacheUIElements();
InitializeUI();
RegisterCallbacks();
}
private void CacheUIElements()
{
uvxPathOverride = Root.Q<TextField>("uv-path-override");
browseUvxButton = Root.Q<Button>("browse-uv-button");
clearUvxButton = Root.Q<Button>("clear-uv-button");
uvxPathStatus = Root.Q<VisualElement>("uv-path-status");
gitUrlOverride = Root.Q<TextField>("git-url-override");
browseGitUrlButton = Root.Q<Button>("browse-git-url-button");
clearGitUrlButton = Root.Q<Button>("clear-git-url-button");
debugLogsToggle = Root.Q<Toggle>("debug-logs-toggle");
devModeForceRefreshToggle = Root.Q<Toggle>("dev-mode-force-refresh-toggle");
useBetaServerToggle = Root.Q<Toggle>("use-beta-server-toggle");
deploySourcePath = Root.Q<TextField>("deploy-source-path");
browseDeploySourceButton = Root.Q<Button>("browse-deploy-source-button");
clearDeploySourceButton = Root.Q<Button>("clear-deploy-source-button");
deployButton = Root.Q<Button>("deploy-button");
deployRestoreButton = Root.Q<Button>("deploy-restore-button");
deployTargetLabel = Root.Q<Label>("deploy-target-label");
deployBackupLabel = Root.Q<Label>("deploy-backup-label");
deployStatusLabel = Root.Q<Label>("deploy-status-label");
healthIndicator = Root.Q<VisualElement>("health-indicator");
healthStatus = Root.Q<Label>("health-status");
testConnectionButton = Root.Q<Button>("test-connection-button");
}
private void InitializeUI()
{
// Set tooltips for fields
if (uvxPathOverride != null)
uvxPathOverride.tooltip = "Override path to uvx executable. Leave empty for auto-detection.";
if (gitUrlOverride != null)
gitUrlOverride.tooltip = "Override server source for uvx --from. Leave empty to use default PyPI package. Example local dev: /path/to/unity-mcp/Server";
if (debugLogsToggle != null)
{
debugLogsToggle.tooltip = "Enable verbose debug logging to the Unity Console.";
var debugLabel = debugLogsToggle?.parent?.Q<Label>();
if (debugLabel != null)
debugLabel.tooltip = debugLogsToggle.tooltip;
}
if (devModeForceRefreshToggle != null)
{
devModeForceRefreshToggle.tooltip = "When enabled, generated uvx commands add '--no-cache --refresh' before launching (slower startup, but avoids stale cached builds while iterating on the Server).";
var forceRefreshLabel = devModeForceRefreshToggle?.parent?.Q<Label>();
if (forceRefreshLabel != null)
forceRefreshLabel.tooltip = devModeForceRefreshToggle.tooltip;
}
if (useBetaServerToggle != null)
{
useBetaServerToggle.tooltip = "When enabled, uvx will fetch the latest beta server version from PyPI. Enable this on the beta branch to get the matching server version.";
var betaServerLabel = useBetaServerToggle?.parent?.Q<Label>();
if (betaServerLabel != null)
betaServerLabel.tooltip = useBetaServerToggle.tooltip;
}
if (testConnectionButton != null)
testConnectionButton.tooltip = "Test the connection between Unity and the MCP server.";
if (deploySourcePath != null)
deploySourcePath.tooltip = "Copy a MCPForUnity folder into this project's package location.";
// Set tooltips for buttons
if (browseUvxButton != null)
browseUvxButton.tooltip = "Browse for uvx executable";
if (clearUvxButton != null)
clearUvxButton.tooltip = "Clear override and use auto-detection";
if (browseGitUrlButton != null)
browseGitUrlButton.tooltip = "Select local server source folder";
if (clearGitUrlButton != null)
clearGitUrlButton.tooltip = "Clear override and use default PyPI package";
if (browseDeploySourceButton != null)
browseDeploySourceButton.tooltip = "Select MCPForUnity source folder";
if (clearDeploySourceButton != null)
clearDeploySourceButton.tooltip = "Clear deployment source path";
if (deployButton != null)
deployButton.tooltip = "Copy MCPForUnity to this project's package location";
if (deployRestoreButton != null)
deployRestoreButton.tooltip = "Restore the last backup before deployment";
gitUrlOverride.value = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");
bool debugEnabled = EditorPrefs.GetBool(EditorPrefKeys.DebugLogs, false);
debugLogsToggle.value = debugEnabled;
McpLog.SetDebugLoggingEnabled(debugEnabled);
devModeForceRefreshToggle.value = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
useBetaServerToggle.value = EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, true);
UpdatePathOverrides();
UpdateDeploymentSection();
}
private void RegisterCallbacks()
{
browseUvxButton.clicked += OnBrowseUvxClicked;
clearUvxButton.clicked += OnClearUvxClicked;
browseGitUrlButton.clicked += OnBrowseGitUrlClicked;
gitUrlOverride.RegisterValueChangedCallback(evt =>
{
string url = evt.newValue?.Trim();
if (string.IsNullOrEmpty(url))
{
EditorPrefs.DeleteKey(EditorPrefKeys.GitUrlOverride);
}
else
{
EditorPrefs.SetString(EditorPrefKeys.GitUrlOverride, url);
}
OnGitUrlChanged?.Invoke();
OnHttpServerCommandUpdateRequested?.Invoke();
});
clearGitUrlButton.clicked += () =>
{
gitUrlOverride.value = string.Empty;
EditorPrefs.DeleteKey(EditorPrefKeys.GitUrlOverride);
OnGitUrlChanged?.Invoke();
OnHttpServerCommandUpdateRequested?.Invoke();
};
debugLogsToggle.RegisterValueChangedCallback(evt =>
{
McpLog.SetDebugLoggingEnabled(evt.newValue);
});
devModeForceRefreshToggle.RegisterValueChangedCallback(evt =>
{
EditorPrefs.SetBool(EditorPrefKeys.DevModeForceServerRefresh, evt.newValue);
OnHttpServerCommandUpdateRequested?.Invoke();
});
useBetaServerToggle.RegisterValueChangedCallback(evt =>
{
EditorPrefs.SetBool(EditorPrefKeys.UseBetaServer, evt.newValue);
OnHttpServerCommandUpdateRequested?.Invoke();
OnBetaModeChanged?.Invoke(evt.newValue);
});
deploySourcePath.RegisterValueChangedCallback(evt =>
{
string path = evt.newValue?.Trim();
if (string.IsNullOrEmpty(path) || path == "Not set")
{
return;
}
try
{
MCPServiceLocator.Deployment.SetStoredSourcePath(path);
}
catch (Exception ex)
{
EditorUtility.DisplayDialog("Invalid Source", ex.Message, "OK");
UpdateDeploymentSection();
}
});
browseDeploySourceButton.clicked += OnBrowseDeploySourceClicked;
clearDeploySourceButton.clicked += OnClearDeploySourceClicked;
deployButton.clicked += OnDeployClicked;
deployRestoreButton.clicked += OnRestoreBackupClicked;
testConnectionButton.clicked += () => OnTestConnectionRequested?.Invoke();
}
public void UpdatePathOverrides()
{
var pathService = MCPServiceLocator.Paths;
bool hasOverride = pathService.HasUvxPathOverride;
bool hasFallback = pathService.HasUvxPathFallback;
string uvxPath = hasOverride ? pathService.GetUvxPath() : null;
// Determine display text based on override and fallback status
if (hasOverride)
{
if (hasFallback)
{
// Override path invalid, using system fallback
string overridePath = EditorPrefs.GetString(EditorPrefKeys.UvxPathOverride, string.Empty);
uvxPathOverride.value = $"Invalid override path: {overridePath} (fallback to uvx path) {uvxPath}";
}
else if (!string.IsNullOrEmpty(uvxPath))
{
// Override path valid
uvxPathOverride.value = uvxPath;
}
else
{
// Override set but invalid, no fallback available
string overridePath = EditorPrefs.GetString(EditorPrefKeys.UvxPathOverride, string.Empty);
uvxPathOverride.value = $"Invalid override path: {overridePath}, no uv found";
}
}
else
{
uvxPathOverride.value = "uvx (uses PATH)";
}
uvxPathStatus.RemoveFromClassList("valid");
uvxPathStatus.RemoveFromClassList("invalid");
uvxPathStatus.RemoveFromClassList("warning");
if (hasOverride)
{
if (hasFallback)
{
// Using fallback - show as warning (yellow)
uvxPathStatus.AddToClassList("warning");
}
else
{
// Override mode: validate the override path
string overridePath = EditorPrefs.GetString(EditorPrefKeys.UvxPathOverride, string.Empty);
if (pathService.TryValidateUvxExecutable(overridePath, out _))
{
uvxPathStatus.AddToClassList("valid");
}
else
{
uvxPathStatus.AddToClassList("invalid");
}
}
}
else
{
// PATH mode: validate system uvx
string systemUvxPath = pathService.GetUvxPath();
if (!string.IsNullOrEmpty(systemUvxPath) && pathService.TryValidateUvxExecutable(systemUvxPath, out _))
{
uvxPathStatus.AddToClassList("valid");
}
else
{
uvxPathStatus.AddToClassList("invalid");
}
}
gitUrlOverride.value = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");
debugLogsToggle.value = EditorPrefs.GetBool(EditorPrefKeys.DebugLogs, false);
devModeForceRefreshToggle.value = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
useBetaServerToggle.value = EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, true);
UpdateDeploymentSection();
}
private void OnBrowseUvxClicked()
{
string suggested = RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
? "/opt/homebrew/bin"
: Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
string picked = EditorUtility.OpenFilePanel("Select uv Executable", suggested, "");
if (!string.IsNullOrEmpty(picked))
{
try
{
MCPServiceLocator.Paths.SetUvxPathOverride(picked);
UpdatePathOverrides();
McpLog.Info($"uv path override set to: {picked}");
}
catch (Exception ex)
{
EditorUtility.DisplayDialog("Invalid Path", ex.Message, "OK");
}
}
}
private void OnClearUvxClicked()
{
MCPServiceLocator.Paths.ClearUvxPathOverride();
UpdatePathOverrides();
McpLog.Info("uv path override cleared");
}
private void OnBrowseGitUrlClicked()
{
string picked = EditorUtility.OpenFolderPanel("Select Server folder", string.Empty, string.Empty);
if (!string.IsNullOrEmpty(picked))
{
gitUrlOverride.value = picked;
EditorPrefs.SetString(EditorPrefKeys.GitUrlOverride, picked);
OnGitUrlChanged?.Invoke();
OnHttpServerCommandUpdateRequested?.Invoke();
McpLog.Info($"Server source override set to: {picked}");
}
}
private void UpdateDeploymentSection()
{
var deployService = MCPServiceLocator.Deployment;
string sourcePath = deployService.GetStoredSourcePath();
deploySourcePath.value = sourcePath ?? string.Empty;
deployTargetLabel.text = $"Target: {deployService.GetTargetDisplayPath()}";
string backupPath = deployService.GetLastBackupPath();
if (deployService.HasBackup())
{
// Use forward slashes to avoid backslash escape sequence issues in UI text
deployBackupLabel.text = $"Last backup: {backupPath?.Replace('\\', '/')}";
}
else
{
deployBackupLabel.text = "Last backup: none";
}
deployRestoreButton?.SetEnabled(deployService.HasBackup());
}
private void OnBrowseDeploySourceClicked()
{
string picked = EditorUtility.OpenFolderPanel("Select MCPForUnity folder", string.Empty, string.Empty);
if (string.IsNullOrEmpty(picked))
{
return;
}
try
{
MCPServiceLocator.Deployment.SetStoredSourcePath(picked);
SetDeployStatus($"Source set: {picked}");
}
catch (Exception ex)
{
EditorUtility.DisplayDialog("Invalid Source", ex.Message, "OK");
SetDeployStatus("Source selection failed");
}
UpdateDeploymentSection();
}
private void OnClearDeploySourceClicked()
{
MCPServiceLocator.Deployment.ClearStoredSourcePath();
UpdateDeploymentSection();
SetDeployStatus("Source cleared");
}
private void OnDeployClicked()
{
var result = MCPServiceLocator.Deployment.DeployFromStoredSource();
SetDeployStatus(result.Message, !result.Success);
if (!result.Success)
{
EditorUtility.DisplayDialog("Deployment Failed", result.Message, "OK");
}
else
{
EditorUtility.DisplayDialog("Deployment Complete", result.Message + (string.IsNullOrEmpty(result.BackupPath) ? string.Empty : $"\nBackup: {result.BackupPath}"), "OK");
}
UpdateDeploymentSection();
}
private void OnRestoreBackupClicked()
{
var result = MCPServiceLocator.Deployment.RestoreLastBackup();
SetDeployStatus(result.Message, !result.Success);
if (!result.Success)
{
EditorUtility.DisplayDialog("Restore Failed", result.Message, "OK");
}
else
{
EditorUtility.DisplayDialog("Restore Complete", result.Message, "OK");
}
UpdateDeploymentSection();
}
private void SetDeployStatus(string message, bool isError = false)
{
if (deployStatusLabel == null)
{
return;
}
deployStatusLabel.text = message;
deployStatusLabel.style.color = isError
? new StyleColor(new Color(0.85f, 0.2f, 0.2f))
: StyleKeyword.Null;
}
public void UpdateHealthStatus(bool isHealthy, string statusText)
{
if (healthStatus != null)
{
healthStatus.text = statusText;
}
if (healthIndicator != null)
{
healthIndicator.RemoveFromClassList("healthy");
healthIndicator.RemoveFromClassList("disconnected");
healthIndicator.RemoveFromClassList("unknown");
if (isHealthy)
{
healthIndicator.AddToClassList("healthy");
}
else if (statusText == HealthStatus.Unknown)
{
healthIndicator.AddToClassList("unknown");
}
else
{
healthIndicator.AddToClassList("disconnected");
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bf87d9c1c3b287e4180379f65af95dca
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,66 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="True">
<Style src="../Common.uss" />
<ui:VisualElement name="advanced-section" class="section">
<ui:Label text="Advanced Settings" class="section-title" />
<ui:VisualElement class="section-content">
<ui:VisualElement class="override-row">
<ui:Label text="UVX Path:" class="override-label" />
<ui:VisualElement class="status-indicator-small" name="uv-path-status" />
</ui:VisualElement>
<ui:VisualElement class="path-override-controls">
<ui:TextField name="uv-path-override" readonly="true" class="override-field" />
<ui:Button name="browse-uv-button" text="Browse" class="icon-button" />
<ui:Button name="clear-uv-button" text="Clear" class="icon-button" />
</ui:VisualElement>
<ui:VisualElement class="override-row" style="margin-top: 8px;">
<ui:Label text="Server Source:" class="override-label" />
</ui:VisualElement>
<ui:VisualElement class="path-override-controls">
<ui:TextField name="git-url-override" placeholder-text="/path/to/Server or git+https://..." class="override-field" />
<ui:Button name="browse-git-url-button" text="Select" class="icon-button" />
<ui:Button name="clear-git-url-button" text="Clear" class="icon-button" />
</ui:VisualElement>
<ui:VisualElement class="setting-row" style="margin-top: 8px;">
<ui:Label text="Debug Logging:" class="setting-label" />
<ui:Toggle name="debug-logs-toggle" class="setting-toggle" />
</ui:VisualElement>
<ui:VisualElement class="setting-row">
<ui:Label text="Server Health:" class="setting-label" />
<ui:VisualElement class="status-container">
<ui:VisualElement name="health-indicator" class="status-dot" />
<ui:Label name="health-status" text="Unknown" class="status-text" />
</ui:VisualElement>
<ui:Button name="test-connection-button" text="Test" class="action-button" />
</ui:VisualElement>
<ui:VisualElement class="setting-row">
<ui:Label text="Force Fresh Install:" class="setting-label" />
<ui:Toggle name="dev-mode-force-refresh-toggle" class="setting-toggle" />
</ui:VisualElement>
<ui:VisualElement class="setting-row">
<ui:Label text="Use Beta Server:" class="setting-label" />
<ui:Toggle name="use-beta-server-toggle" class="setting-toggle" />
</ui:VisualElement>
<ui:VisualElement class="override-row" style="margin-top: 8px;">
<ui:Label text="Package Source:" class="override-label" />
</ui:VisualElement>
<ui:VisualElement class="path-override-controls">
<ui:TextField name="deploy-source-path" class="override-field" />
<ui:Button name="browse-deploy-source-button" text="Select" class="icon-button" />
<ui:Button name="clear-deploy-source-button" text="Clear" class="icon-button" />
</ui:VisualElement>
<ui:Label name="deploy-target-label" class="help-text" />
<ui:Label name="deploy-backup-label" class="help-text" />
<ui:VisualElement class="path-override-controls" style="margin-top: 4px;">
<ui:Button name="deploy-button" text="Deploy" class="icon-button" />
<ui:Button name="deploy-restore-button" text="Restore" class="icon-button" />
</ui:VisualElement>
<ui:Label name="deploy-status-label" class="help-text" />
</ui:VisualElement>
</ui:VisualElement>
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: d7e63a0b220a4c9458289415ad91e7df
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4d9f5ceeb24166f47804e094440b7846
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,574 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using MCPForUnity.Editor.Clients;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Models;
using MCPForUnity.Editor.Services;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace MCPForUnity.Editor.Windows.Components.ClientConfig
{
/// <summary>
/// Controller for the Client Configuration section of the MCP For Unity editor window.
/// Handles client selection, configuration, status display, and manual configuration details.
/// </summary>
public class McpClientConfigSection
{
// UI Elements
private DropdownField clientDropdown;
private Button configureAllButton;
private VisualElement clientStatusIndicator;
private Label clientStatusLabel;
private Button configureButton;
private VisualElement claudeCliPathRow;
private TextField claudeCliPath;
private Button browseClaudeButton;
private Foldout manualConfigFoldout;
private TextField configPathField;
private Button copyPathButton;
private Button openFileButton;
private TextField configJsonField;
private Button copyJsonButton;
private Label installationStepsLabel;
// Data
private readonly List<IMcpClientConfigurator> configurators;
private readonly Dictionary<IMcpClientConfigurator, DateTime> lastStatusChecks = new();
private readonly HashSet<IMcpClientConfigurator> statusRefreshInFlight = new();
private static readonly TimeSpan StatusRefreshInterval = TimeSpan.FromSeconds(45);
private int selectedClientIndex = 0;
// Events
/// <summary>
/// Fired when the selected client's configured transport is detected/updated.
/// The parameter contains the client name and its configured transport.
/// </summary>
public event Action<string, ConfiguredTransport> OnClientTransportDetected;
public VisualElement Root { get; private set; }
public McpClientConfigSection(VisualElement root)
{
Root = root;
configurators = MCPServiceLocator.Client.GetAllClients().ToList();
CacheUIElements();
InitializeUI();
RegisterCallbacks();
}
private void CacheUIElements()
{
clientDropdown = Root.Q<DropdownField>("client-dropdown");
configureAllButton = Root.Q<Button>("configure-all-button");
clientStatusIndicator = Root.Q<VisualElement>("client-status-indicator");
clientStatusLabel = Root.Q<Label>("client-status");
configureButton = Root.Q<Button>("configure-button");
claudeCliPathRow = Root.Q<VisualElement>("claude-cli-path-row");
claudeCliPath = Root.Q<TextField>("claude-cli-path");
browseClaudeButton = Root.Q<Button>("browse-claude-button");
manualConfigFoldout = Root.Q<Foldout>("manual-config-foldout");
configPathField = Root.Q<TextField>("config-path");
copyPathButton = Root.Q<Button>("copy-path-button");
openFileButton = Root.Q<Button>("open-file-button");
configJsonField = Root.Q<TextField>("config-json");
copyJsonButton = Root.Q<Button>("copy-json-button");
installationStepsLabel = Root.Q<Label>("installation-steps");
}
private void InitializeUI()
{
// Ensure manual config foldout starts collapsed
if (manualConfigFoldout != null)
{
manualConfigFoldout.value = false;
}
var clientNames = configurators.Select(c => c.DisplayName).ToList();
clientDropdown.choices = clientNames;
if (clientNames.Count > 0)
{
clientDropdown.index = 0;
}
claudeCliPathRow.style.display = DisplayStyle.None;
// Initialize the configuration display for the first selected client
UpdateClientStatus();
UpdateManualConfiguration();
UpdateClaudeCliPathVisibility();
}
private void RegisterCallbacks()
{
clientDropdown.RegisterValueChangedCallback(evt =>
{
selectedClientIndex = clientDropdown.index;
UpdateClientStatus();
UpdateManualConfiguration();
UpdateClaudeCliPathVisibility();
});
configureAllButton.clicked += OnConfigureAllClientsClicked;
configureButton.clicked += OnConfigureClicked;
browseClaudeButton.clicked += OnBrowseClaudeClicked;
copyPathButton.clicked += OnCopyPathClicked;
openFileButton.clicked += OnOpenFileClicked;
copyJsonButton.clicked += OnCopyJsonClicked;
}
public void UpdateClientStatus()
{
if (selectedClientIndex < 0 || selectedClientIndex >= configurators.Count)
return;
var client = configurators[selectedClientIndex];
RefreshClientStatus(client);
}
private string GetStatusDisplayString(McpStatus status)
{
return status switch
{
McpStatus.NotConfigured => "Not Configured",
McpStatus.Configured => "Configured",
McpStatus.Running => "Running",
McpStatus.Connected => "Connected",
McpStatus.IncorrectPath => "Incorrect Path",
McpStatus.CommunicationError => "Communication Error",
McpStatus.NoResponse => "No Response",
McpStatus.UnsupportedOS => "Unsupported OS",
McpStatus.MissingConfig => "Missing MCPForUnity Config",
McpStatus.Error => "Error",
_ => "Unknown",
};
}
public void UpdateManualConfiguration()
{
if (selectedClientIndex < 0 || selectedClientIndex >= configurators.Count)
return;
var client = configurators[selectedClientIndex];
string configPath = client.GetConfigPath();
configPathField.value = configPath;
string configJson = client.GetManualSnippet();
configJsonField.value = configJson;
var steps = client.GetInstallationSteps();
if (steps != null && steps.Count > 0)
{
var numbered = steps.Select((s, i) => $"{i + 1}. {s}");
installationStepsLabel.text = string.Join("\n", numbered);
}
else
{
installationStepsLabel.text = "Configuration steps not available for this client.";
}
}
private void UpdateClaudeCliPathVisibility()
{
if (selectedClientIndex < 0 || selectedClientIndex >= configurators.Count)
return;
var client = configurators[selectedClientIndex];
if (client is ClaudeCliMcpConfigurator)
{
string claudePath = MCPServiceLocator.Paths.GetClaudeCliPath();
if (string.IsNullOrEmpty(claudePath))
{
claudeCliPathRow.style.display = DisplayStyle.Flex;
claudeCliPath.value = "Not found - click Browse to select";
}
else
{
claudeCliPathRow.style.display = DisplayStyle.Flex;
claudeCliPath.value = claudePath;
}
}
else
{
claudeCliPathRow.style.display = DisplayStyle.None;
}
}
private void OnConfigureAllClientsClicked()
{
try
{
var summary = MCPServiceLocator.Client.ConfigureAllDetectedClients();
string message = summary.GetSummaryMessage() + "\n\n";
foreach (var msg in summary.Messages)
{
message += msg + "\n";
}
EditorUtility.DisplayDialog("Configure All Clients", message, "OK");
if (selectedClientIndex >= 0 && selectedClientIndex < configurators.Count)
{
UpdateClientStatus();
UpdateManualConfiguration();
}
}
catch (Exception ex)
{
EditorUtility.DisplayDialog("Configuration Failed", ex.Message, "OK");
}
}
private void OnConfigureClicked()
{
if (selectedClientIndex < 0 || selectedClientIndex >= configurators.Count)
return;
var client = configurators[selectedClientIndex];
// Handle CLI configurators asynchronously
if (client is ClaudeCliMcpConfigurator)
{
ConfigureClaudeCliAsync(client);
return;
}
try
{
MCPServiceLocator.Client.ConfigureClient(client);
lastStatusChecks.Remove(client);
RefreshClientStatus(client, forceImmediate: true);
UpdateManualConfiguration();
}
catch (Exception ex)
{
clientStatusLabel.text = "Error";
clientStatusLabel.style.color = Color.red;
McpLog.Error($"Configuration failed: {ex.Message}");
EditorUtility.DisplayDialog("Configuration Failed", ex.Message, "OK");
}
}
private void ConfigureClaudeCliAsync(IMcpClientConfigurator client)
{
if (statusRefreshInFlight.Contains(client))
return;
statusRefreshInFlight.Add(client);
bool isCurrentlyConfigured = client.Status == McpStatus.Configured;
ApplyStatusToUi(client, showChecking: true, customMessage: isCurrentlyConfigured ? "Unregistering..." : "Registering...");
// Capture ALL main-thread-only values before async task
string projectDir = Path.GetDirectoryName(Application.dataPath);
bool useHttpTransport = EditorConfigurationCache.Instance.UseHttpTransport;
string claudePath = MCPServiceLocator.Paths.GetClaudeCliPath();
string httpUrl = HttpEndpointUtility.GetMcpRpcUrl();
var (uvxPath, gitUrl, packageName) = AssetPathUtility.GetUvxCommandParts();
bool shouldForceRefresh = AssetPathUtility.ShouldForceUvxRefresh();
string apiKey = EditorPrefs.GetString(EditorPrefKeys.ApiKey, string.Empty);
// Compute pathPrepend on main thread
string pathPrepend = null;
if (Application.platform == RuntimePlatform.OSXEditor)
pathPrepend = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
else if (Application.platform == RuntimePlatform.LinuxEditor)
pathPrepend = "/usr/local/bin:/usr/bin:/bin";
try
{
string claudeDir = Path.GetDirectoryName(claudePath);
if (!string.IsNullOrEmpty(claudeDir))
pathPrepend = string.IsNullOrEmpty(pathPrepend) ? claudeDir : $"{claudeDir}:{pathPrepend}";
}
catch { }
Task.Run(() =>
{
try
{
if (client is ClaudeCliMcpConfigurator cliConfigurator)
{
var serverTransport = HttpEndpointUtility.GetCurrentServerTransport();
cliConfigurator.ConfigureWithCapturedValues(
projectDir, claudePath, pathPrepend,
useHttpTransport, httpUrl,
uvxPath, gitUrl, packageName, shouldForceRefresh,
apiKey, serverTransport);
}
return (success: true, error: (string)null);
}
catch (Exception ex)
{
return (success: false, error: ex.Message);
}
}).ContinueWith(t =>
{
string errorMessage = null;
if (t.IsFaulted && t.Exception != null)
{
errorMessage = t.Exception.GetBaseException()?.Message ?? "Configuration failed";
}
else if (!t.Result.success)
{
errorMessage = t.Result.error;
}
EditorApplication.delayCall += () =>
{
statusRefreshInFlight.Remove(client);
lastStatusChecks.Remove(client);
if (errorMessage != null)
{
if (client is McpClientConfiguratorBase baseConfigurator)
{
baseConfigurator.Client.SetStatus(McpStatus.Error, errorMessage);
}
McpLog.Error($"Configuration failed: {errorMessage}");
RefreshClientStatus(client, forceImmediate: true);
}
else
{
// Registration succeeded - trust the status set by RegisterWithCapturedValues
// and update UI without re-verifying (which could fail due to CLI timing/scope issues)
lastStatusChecks[client] = DateTime.UtcNow;
ApplyStatusToUi(client);
}
UpdateManualConfiguration();
};
});
}
private void OnBrowseClaudeClicked()
{
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))
{
try
{
MCPServiceLocator.Paths.SetClaudeCliPathOverride(picked);
UpdateClaudeCliPathVisibility();
UpdateClientStatus();
McpLog.Info($"Claude CLI path override set to: {picked}");
}
catch (Exception ex)
{
EditorUtility.DisplayDialog("Invalid Path", ex.Message, "OK");
}
}
}
private void OnCopyPathClicked()
{
EditorGUIUtility.systemCopyBuffer = configPathField.value;
McpLog.Info("Config path copied to clipboard");
}
private void OnOpenFileClicked()
{
string path = configPathField.value;
try
{
if (!File.Exists(path))
{
EditorUtility.DisplayDialog("Open File", "The configuration file path does not exist.", "OK");
return;
}
Process.Start(new ProcessStartInfo
{
FileName = path,
UseShellExecute = true
});
}
catch (Exception ex)
{
McpLog.Error($"Failed to open file: {ex.Message}");
}
}
private void OnCopyJsonClicked()
{
EditorGUIUtility.systemCopyBuffer = configJsonField.value;
McpLog.Info("Configuration copied to clipboard");
}
public void RefreshSelectedClient(bool forceImmediate = false)
{
if (selectedClientIndex >= 0 && selectedClientIndex < configurators.Count)
{
var client = configurators[selectedClientIndex];
// Force immediate for non-Claude CLI, or when explicitly requested
bool shouldForceImmediate = forceImmediate || client is not ClaudeCliMcpConfigurator;
RefreshClientStatus(client, shouldForceImmediate);
UpdateManualConfiguration();
UpdateClaudeCliPathVisibility();
}
}
private void RefreshClientStatus(IMcpClientConfigurator client, bool forceImmediate = false)
{
if (client is ClaudeCliMcpConfigurator)
{
RefreshClaudeCliStatus(client, forceImmediate);
return;
}
if (forceImmediate || ShouldRefreshClient(client))
{
MCPServiceLocator.Client.CheckClientStatus(client);
lastStatusChecks[client] = DateTime.UtcNow;
}
ApplyStatusToUi(client);
}
private void RefreshClaudeCliStatus(IMcpClientConfigurator client, bool forceImmediate)
{
bool hasStatus = lastStatusChecks.ContainsKey(client);
bool needsRefresh = !hasStatus || ShouldRefreshClient(client);
if (!hasStatus)
{
ApplyStatusToUi(client, showChecking: true);
}
else
{
ApplyStatusToUi(client);
}
if ((forceImmediate || needsRefresh) && !statusRefreshInFlight.Contains(client))
{
statusRefreshInFlight.Add(client);
ApplyStatusToUi(client, showChecking: true);
// Capture main-thread-only values before async task
string projectDir = Path.GetDirectoryName(Application.dataPath);
bool useHttpTransport = EditorConfigurationCache.Instance.UseHttpTransport;
string claudePath = MCPServiceLocator.Paths.GetClaudeCliPath();
Task.Run(() =>
{
// Defensive: RefreshClientStatus routes Claude CLI clients here, but avoid hard-cast
// so accidental future call sites can't crash the UI.
if (client is ClaudeCliMcpConfigurator claudeConfigurator)
{
// Use thread-safe version with captured main-thread values
claudeConfigurator.CheckStatusWithProjectDir(projectDir, useHttpTransport, claudePath, attemptAutoRewrite: false);
}
}).ContinueWith(t =>
{
bool faulted = false;
string errorMessage = null;
if (t.IsFaulted && t.Exception != null)
{
var baseException = t.Exception.GetBaseException();
errorMessage = baseException?.Message ?? "Status check failed";
McpLog.Error($"Failed to refresh Claude CLI status: {errorMessage}");
faulted = true;
}
EditorApplication.delayCall += () =>
{
statusRefreshInFlight.Remove(client);
lastStatusChecks[client] = DateTime.UtcNow;
if (faulted)
{
if (client is McpClientConfiguratorBase baseConfigurator)
{
baseConfigurator.Client.SetStatus(McpStatus.Error, errorMessage ?? "Status check failed");
}
}
ApplyStatusToUi(client);
};
});
}
}
private bool ShouldRefreshClient(IMcpClientConfigurator client)
{
if (!lastStatusChecks.TryGetValue(client, out var last))
{
return true;
}
return (DateTime.UtcNow - last) > StatusRefreshInterval;
}
private void ApplyStatusToUi(IMcpClientConfigurator client, bool showChecking = false, string customMessage = null)
{
if (selectedClientIndex < 0 || selectedClientIndex >= configurators.Count)
return;
if (!ReferenceEquals(configurators[selectedClientIndex], client))
return;
clientStatusIndicator.RemoveFromClassList("configured");
clientStatusIndicator.RemoveFromClassList("not-configured");
clientStatusIndicator.RemoveFromClassList("warning");
if (showChecking)
{
clientStatusLabel.text = customMessage ?? "Checking...";
clientStatusLabel.style.color = StyleKeyword.Null;
clientStatusIndicator.AddToClassList("warning");
configureButton.text = client.GetConfigureActionLabel();
return;
}
// Check for transport mismatch (3-way: Stdio, Http, HttpRemote)
bool hasTransportMismatch = false;
if (client.ConfiguredTransport != ConfiguredTransport.Unknown)
{
ConfiguredTransport serverTransport = HttpEndpointUtility.GetCurrentServerTransport();
hasTransportMismatch = client.ConfiguredTransport != serverTransport;
}
// If configured but with transport mismatch, show warning state
if (hasTransportMismatch && (client.Status == McpStatus.Configured || client.Status == McpStatus.Running || client.Status == McpStatus.Connected))
{
clientStatusLabel.text = "Transport Mismatch";
clientStatusIndicator.AddToClassList("warning");
}
else
{
clientStatusLabel.text = GetStatusDisplayString(client.Status);
switch (client.Status)
{
case McpStatus.Configured:
case McpStatus.Running:
case McpStatus.Connected:
clientStatusIndicator.AddToClassList("configured");
break;
case McpStatus.IncorrectPath:
case McpStatus.CommunicationError:
case McpStatus.NoResponse:
clientStatusIndicator.AddToClassList("warning");
break;
default:
clientStatusIndicator.AddToClassList("not-configured");
break;
}
}
clientStatusLabel.style.color = StyleKeyword.Null;
configureButton.text = client.GetConfigureActionLabel();
// Notify listeners about the client's configured transport
OnClientTransportDetected?.Invoke(client.DisplayName, client.ConfiguredTransport);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5e29d88664440184e9c0165aadf02d46
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,42 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="True">
<Style src="../Common.uss" />
<ui:VisualElement name="client-section" class="section">
<ui:Label text="Client Configuration" class="section-title" />
<ui:VisualElement class="section-content">
<ui:VisualElement class="setting-row">
<ui:Label text="Client:" class="setting-label" />
<ui:DropdownField name="client-dropdown" class="setting-dropdown-inline" />
</ui:VisualElement>
<ui:VisualElement class="setting-row">
<ui:VisualElement class="status-container">
<ui:VisualElement name="client-status-indicator" class="status-dot" />
<ui:Label name="client-status" text="Not Configured" class="status-text" />
</ui:VisualElement>
<ui:Button name="configure-button" text="Configure" class="action-button" />
</ui:VisualElement>
<ui:VisualElement class="setting-row" name="claude-cli-path-row" style="display: none;">
<ui:Label text="Claude CLI Path:" class="setting-label-small" />
<ui:TextField name="claude-cli-path" readonly="true" class="path-display-field" />
<ui:Button name="browse-claude-button" text="Browse" class="icon-button" />
</ui:VisualElement>
<ui:Foldout name="manual-config-foldout" text="Manual Configuration" value="false" class="manual-config-foldout">
<ui:VisualElement class="manual-config-content">
<ui:Label text="Config Path:" class="config-label" />
<ui:VisualElement class="path-row">
<ui:TextField name="config-path" readonly="true" class="config-path-field" />
<ui:Button name="copy-path-button" text="Copy" class="icon-button" />
<ui:Button name="open-file-button" text="Open" class="icon-button" />
</ui:VisualElement>
<ui:Label text="Configuration:" class="config-label" />
<ui:VisualElement class="config-json-row">
<ui:TextField name="config-json" readonly="true" multiline="true" class="config-json-field" />
<ui:Button name="copy-json-button" text="Copy" class="icon-button-vertical" />
</ui:VisualElement>
<ui:Label text="Installation Steps:" class="config-label" />
<ui:Label name="installation-steps" class="installation-steps" />
</ui:VisualElement>
</ui:Foldout>
<ui:Button name="configure-all-button" text="Configure All Detected Clients" class="secondary-button" style="width: auto; padding-left: 12px; padding-right: 12px;" />
</ui:VisualElement>
</ui:VisualElement>
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: abdb7edaa375af049bd795c7a8b8a613
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,615 @@
/* Root Container */
#root-container {
padding: 16px;
flex-shrink: 1;
overflow: hidden;
}
/* Title */
.title {
font-size: 20px;
-unity-font-style: bold;
margin-bottom: 20px;
padding: 8px;
background-color: rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
/* Section Styling */
.section {
margin-bottom: 14px;
padding: 8px;
background-color: rgba(0, 0, 0, 0.04);
border-radius: 4px;
border-width: 1px;
border-color: rgba(0, 0, 0, 0.15);
}
/* Remove bottom margin from last section in a stack */
/* Note: Unity UI Toolkit doesn't support :last-child pseudo-class.
The .section-last class is applied programmatically instead. */
.section-stack > .section.section-last {
margin-bottom: 0px;
}
.section-title {
font-size: 13px;
-unity-font-style: bold;
margin-bottom: 8px;
padding-bottom: 6px;
letter-spacing: 0.3px;
border-bottom-width: 1px;
border-bottom-color: rgba(255, 255, 255, 0.08);
}
.section-content {
padding: 4px;
}
/* Setting Rows */
.setting-row {
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
min-height: 24px;
flex-shrink: 1;
min-width: 0;
}
.setting-column {
flex-direction: column;
margin-bottom: 4px;
flex-shrink: 1;
min-width: 0;
}
.setting-label {
min-width: 140px;
flex-shrink: 0;
-unity-text-align: middle-left;
}
.setting-value {
-unity-text-align: middle-right;
color: rgba(150, 150, 150, 1);
}
.setting-toggle {
margin-left: auto;
}
.setting-dropdown {
flex-grow: 1;
flex-shrink: 1;
margin-top: 4px;
max-width: 100%;
}
.setting-dropdown-inline {
flex-grow: 1;
flex-shrink: 1;
min-width: 150px;
flex-basis: 200px;
}
/* Validation Description */
.validation-description {
margin-top: 4px;
padding: 8px;
background-color: rgba(100, 150, 200, 0.15);
border-radius: 4px;
font-size: 11px;
white-space: normal;
}
/* Port Fields */
.port-field {
width: 100px;
margin-left: auto;
}
/* URL Fields */
.url-field {
flex-grow: 1;
flex-shrink: 1;
min-width: 200px;
flex-basis: 250px;
}
.port-field > .unity-text-field__input {
background-color: rgba(0, 0, 0, 0.1);
-unity-text-align: middle-center;
}
/* Status Container */
.status-container {
flex-direction: row;
align-items: center;
flex-grow: 1;
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 6px;
margin-right: 8px;
background-color: rgba(150, 150, 150, 1);
}
.status-dot.connected {
background-color: rgba(0, 200, 100, 1);
}
.status-dot.disconnected {
background-color: rgba(200, 50, 50, 1);
}
.status-dot.configured {
background-color: rgba(0, 200, 100, 1);
}
.status-dot.not-configured {
background-color: rgba(200, 50, 50, 1);
}
.status-dot.warning {
background-color: rgba(255, 200, 0, 1);
}
.status-dot.unknown {
background-color: rgba(150, 150, 150, 1);
}
.status-dot.healthy {
background-color: rgba(0, 200, 100, 1);
}
.status-text {
-unity-text-align: middle-left;
}
.status-indicator-small {
width: 8px;
height: 8px;
border-radius: 4px;
margin-left: 8px;
background-color: rgba(150, 150, 150, 1);
}
.status-indicator-small.valid {
background-color: rgba(0, 200, 100, 1);
}
.status-indicator-small.invalid {
background-color: rgba(200, 50, 50, 1);
}
.status-indicator-small.warning {
background-color: rgba(255, 200, 0, 1);
}
/* Buttons */
.action-button {
min-width: 80px;
height: 28px;
margin-left: 8px;
background-color: rgba(50, 150, 250, 0.8);
border-radius: 4px;
-unity-font-style: bold;
}
.action-button:hover {
background-color: rgba(50, 150, 250, 1);
}
.action-button:active {
background-color: rgba(30, 120, 200, 1);
}
/* Start Server button in the manual config section should align flush left like other full-width controls */
.start-server-button {
margin-left: 0px;
}
/* When the HTTP server/session is running, we show the Start/Stop button as "danger" (red) */
.action-button.server-running {
background-color: rgba(200, 50, 50, 0.85);
}
.action-button.server-running:hover {
background-color: rgba(220, 60, 60, 1);
}
.action-button.server-running:active {
background-color: rgba(170, 40, 40, 1);
}
.secondary-button {
width: 100%;
height: 28px;
background-color: rgba(100, 100, 100, 0.3);
border-radius: 4px;
}
.secondary-button:hover {
background-color: rgba(100, 100, 100, 0.5);
}
.secondary-button:active {
background-color: rgba(80, 80, 80, 0.5);
}
/* Manual Configuration */
.manual-config-content {
padding: 8px;
margin-top: 8px;
background-color: rgba(0, 0, 0, 0.05);
border-radius: 4px;
flex-shrink: 1;
min-width: 0;
}
.config-label {
font-size: 11px;
-unity-font-style: bold;
margin-top: 8px;
margin-bottom: 4px;
}
.path-row {
flex-direction: row;
align-items: center;
margin-bottom: 8px;
flex-shrink: 1;
min-width: 0;
}
.config-path-field {
flex-grow: 1;
flex-shrink: 1;
margin-right: 4px;
min-width: 0;
max-width: 100%;
}
.config-path-field > .unity-text-field__input {
background-color: rgba(0, 0, 0, 0.1);
font-size: 11px;
overflow: hidden;
text-overflow: clip;
}
.icon-button {
min-width: 50px;
height: 24px;
margin-left: 4px;
}
.config-json-row {
flex-direction: row;
margin-bottom: 8px;
flex-shrink: 1;
min-width: 0;
}
.config-json-field {
flex-grow: 1;
flex-shrink: 1;
min-height: 64px;
margin-right: 4px;
min-width: 0;
max-width: 100%;
}
.config-json-field > .unity-text-field__input {
background-color: rgba(0, 0, 0, 0.1);
font-size: 10px;
-unity-font-style: normal;
white-space: normal;
overflow: hidden;
}
.icon-button-vertical {
min-width: 50px;
height: 30px;
align-self: flex-start;
}
.installation-steps {
padding: 8px;
background-color: rgba(0, 0, 0, 0.05);
border-radius: 4px;
font-size: 11px;
white-space: normal;
margin-top: 4px;
}
/* Tools Section */
.tool-actions {
flex-direction: row;
flex-wrap: wrap;
margin-top: 8px;
margin-bottom: 8px;
}
.tool-action-button {
flex-grow: 1;
min-width: 0;
height: 26px;
margin-bottom: 4px;
}
.tool-category-container {
flex-direction: column;
margin-top: 8px;
}
.tool-item {
flex-direction: column;
padding: 8px;
margin-bottom: 8px;
background-color: rgba(0, 0, 0, 0.04);
border-radius: 4px;
border-width: 1px;
border-color: rgba(0, 0, 0, 0.12);
}
.tool-item-header {
flex-direction: row;
align-items: center;
}
.tool-item-toggle {
flex-shrink: 0;
min-width: 0;
}
.tool-tags {
flex-direction: row;
flex-wrap: wrap;
margin-left: auto;
padding-left: 8px;
}
.tool-tag {
font-size: 10px;
padding: 2px 6px;
margin-left: 4px;
margin-top: 2px;
background-color: rgba(100, 100, 100, 0.25);
border-radius: 3px;
color: rgba(40, 40, 40, 1);
}
.tool-item-description,
.tool-parameters {
font-size: 11px;
color: rgba(120, 120, 120, 1);
white-space: normal;
margin-top: 4px;
}
.tool-parameters {
-unity-font-style: italic;
}
/* Advanced Settings */
.advanced-settings-foldout {
margin-top: 16px;
}
.advanced-settings-foldout > .unity-foldout__toggle {
-unity-font-style: bold;
font-size: 12px;
}
/* Manual Command Foldout */
.manual-command-foldout {
margin-top: 8px;
margin-bottom: 8px;
}
.manual-command-foldout > .unity-foldout__toggle {
font-size: 11px;
padding: 4px;
background-color: rgba(0, 0, 0, 0.05);
border-radius: 3px;
}
.manual-command-foldout > .unity-foldout__content {
margin-top: 4px;
}
.advanced-settings-content {
padding: 8px;
margin-top: 8px;
background-color: rgba(0, 0, 0, 0.05);
border-radius: 4px;
flex-shrink: 1;
min-width: 0;
}
.advanced-label {
font-size: 11px;
-unity-font-style: bold;
margin-bottom: 8px;
color: rgba(150, 150, 150, 1);
white-space: normal;
flex-shrink: 1;
min-width: 0;
}
.override-row {
flex-direction: row;
align-items: center;
margin-top: 8px;
margin-bottom: 4px;
}
.override-label {
font-size: 11px;
-unity-font-style: bold;
min-width: 120px;
white-space: normal;
flex-shrink: 1;
}
.help-text {
font-size: 10px;
color: rgba(120, 120, 120, 1);
white-space: normal;
flex-shrink: 1;
min-width: 0;
margin-bottom: 4px;
}
.help-text.http-local-url-error {
color: rgba(255, 80, 80, 1);
-unity-font-style: bold;
}
.path-override-controls {
flex-direction: row;
align-items: center;
margin-bottom: 12px;
flex-shrink: 1;
min-width: 0;
}
.override-field {
flex-grow: 1;
flex-shrink: 1;
margin-right: 4px;
min-width: 0;
max-width: 100%;
}
.override-field > .unity-text-field__input {
background-color: rgba(0, 0, 0, 0.1);
font-size: 11px;
overflow: hidden;
text-overflow: clip;
}
.setting-label-small {
font-size: 11px;
min-width: 120px;
-unity-text-align: middle-left;
}
.path-display-field {
flex-grow: 1;
flex-shrink: 1;
margin-right: 4px;
min-width: 0;
max-width: 100%;
}
.path-display-field > .unity-text-field__input {
background-color: rgba(0, 0, 0, 0.1);
font-size: 11px;
overflow: hidden;
text-overflow: clip;
}
/* Light Theme Overrides */
.unity-theme-light .title {
background-color: rgba(0, 0, 0, 0.05);
}
.unity-theme-light .section {
background-color: rgba(0, 0, 0, 0.03);
border-color: rgba(0, 0, 0, 0.15);
}
.unity-theme-light .section-title {
border-bottom-color: rgba(0, 0, 0, 0.1);
}
.unity-theme-dark .tool-tag {
color: rgba(220, 220, 220, 1);
background-color: rgba(80, 80, 80, 0.6);
}
.unity-theme-dark .tool-item {
background-color: rgba(255, 255, 255, 0.04);
border-color: rgba(255, 255, 255, 0.08);
}
.unity-theme-dark .tool-item-description,
.unity-theme-dark .tool-parameters {
color: rgba(200, 200, 200, 0.8);
}
.unity-theme-light .validation-description {
background-color: rgba(100, 150, 200, 0.1);
}
.unity-theme-light .port-field > .unity-text-field__input {
background-color: rgba(0, 0, 0, 0.05);
}
.unity-theme-light .config-path-field > .unity-text-field__input {
background-color: rgba(0, 0, 0, 0.05);
}
.unity-theme-light .config-json-field > .unity-text-field__input {
background-color: rgba(0, 0, 0, 0.05);
}
.unity-theme-light .manual-config-content {
background-color: rgba(0, 0, 0, 0.03);
}
.unity-theme-light .installation-steps {
background-color: rgba(0, 0, 0, 0.03);
}
.unity-theme-light .advanced-settings-content {
background-color: rgba(0, 0, 0, 0.03);
}
.unity-theme-light .override-field > .unity-text-field__input {
background-color: rgba(0, 0, 0, 0.05);
}
.unity-theme-light .path-display-field > .unity-text-field__input {
background-color: rgba(0, 0, 0, 0.05);
}
/* Warning Banner (for transport mismatch, etc.) */
.warning-banner {
display: none;
padding: 8px 12px;
margin-bottom: 6px;
background-color: rgba(255, 180, 0, 0.2);
border-radius: 4px;
border-width: 1px;
border-color: rgba(255, 180, 0, 0.5);
}
.warning-banner.visible {
display: flex;
}
.warning-banner-text {
font-size: 11px;
white-space: normal;
color: rgba(180, 120, 0, 1);
}
.unity-theme-dark .warning-banner {
background-color: rgba(255, 180, 0, 0.15);
border-color: rgba(255, 180, 0, 0.4);
}
.unity-theme-dark .warning-banner-text {
color: rgba(255, 200, 100, 1);
}
.unity-theme-light .manual-command-foldout > .unity-foldout__toggle {
background-color: rgba(0, 0, 0, 0.03);
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2cccfdd3f4371f140902a54e247cf979
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 23563b155b001a14c8263aa095cd527b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a3fae4a627f640749a80d5d1dc84ebe4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,54 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="True">
<Style src="../Common.uss" />
<ui:VisualElement name="connection-section" class="section">
<ui:Label text="Server" class="section-title" />
<ui:VisualElement class="section-content">
<ui:VisualElement class="setting-row">
<ui:Label text="Transport:" class="setting-label" />
<uie:EnumField name="transport-dropdown" class="setting-dropdown-inline" />
</ui:VisualElement>
<ui:VisualElement name="transport-mismatch-warning" class="warning-banner">
<ui:Label name="transport-mismatch-text" class="warning-banner-text" />
</ui:VisualElement>
<ui:VisualElement class="setting-row" name="http-url-row">
<ui:Label text="HTTP URL:" class="setting-label" />
<ui:TextField name="http-url" class="url-field" />
</ui:VisualElement>
<ui:VisualElement name="api-key-row" style="margin-bottom: 4px;">
<ui:VisualElement class="setting-row">
<ui:Label text="API Key:" class="setting-label" />
<ui:TextField name="api-key-field" password="true" class="url-field" />
</ui:VisualElement>
<ui:VisualElement style="flex-direction: row; justify-content: flex-end;">
<ui:Button name="get-api-key-button" text="Get API Key" class="action-button" />
<ui:Button name="clear-api-key-button" text="Clear" class="action-button" />
</ui:VisualElement>
</ui:VisualElement>
<ui:VisualElement class="setting-row" name="http-server-control-row">
<ui:Label text="Local Server:" class="setting-label" />
<ui:Button name="start-http-server-button" text="Start Server" class="action-button start-server-button" />
</ui:VisualElement>
<ui:VisualElement class="setting-row" name="unity-socket-port-row">
<ui:Label text="Unity Socket Port:" class="setting-label" />
<ui:TextField name="unity-port" class="port-field" />
</ui:VisualElement>
<ui:VisualElement class="setting-row">
<ui:VisualElement class="status-container">
<ui:VisualElement name="status-indicator" class="status-dot" />
<ui:Label name="connection-status" text="Disconnected" class="status-text" />
</ui:VisualElement>
<ui:Button name="connection-toggle" text="Start" class="action-button" />
</ui:VisualElement>
<ui:Foldout name="manual-command-foldout" text="Manual Server Launch" value="false" class="manual-config-foldout">
<ui:VisualElement name="http-server-command-section" class="manual-config-content">
<ui:Label text="Use this command to launch the server manually:" class="config-label" />
<ui:VisualElement class="config-json-row">
<ui:TextField name="http-server-command" readonly="true" multiline="true" class="config-json-field" />
<ui:Button name="copy-http-server-command-button" text="Copy" class="icon-button-vertical" />
</ui:VisualElement>
<ui:Label name="http-server-command-hint" class="help-text" />
</ui:VisualElement>
</ui:Foldout>
</ui:VisualElement>
</ui:VisualElement>
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 9943fa234c3a76a4198d2983bf96ab26
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 582ec97120b80401cb943b45d15425f9
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,250 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Services;
using UnityEditor;
using UnityEngine.UIElements;
namespace MCPForUnity.Editor.Windows.Components.Resources
{
/// <summary>
/// Controller for the Resources section inside the MCP For Unity editor window.
/// Provides discovery, filtering, and per-resource enablement toggles.
/// </summary>
public class McpResourcesSection
{
private readonly Dictionary<string, Toggle> resourceToggleMap = new();
private Label summaryLabel;
private Label noteLabel;
private Button enableAllButton;
private Button disableAllButton;
private Button rescanButton;
private VisualElement categoryContainer;
private List<ResourceMetadata> allResources = new();
public VisualElement Root { get; }
public McpResourcesSection(VisualElement root)
{
Root = root;
CacheUIElements();
RegisterCallbacks();
}
private void CacheUIElements()
{
summaryLabel = Root.Q<Label>("resources-summary");
noteLabel = Root.Q<Label>("resources-note");
enableAllButton = Root.Q<Button>("enable-all-resources-button");
disableAllButton = Root.Q<Button>("disable-all-resources-button");
rescanButton = Root.Q<Button>("rescan-resources-button");
categoryContainer = Root.Q<VisualElement>("resource-category-container");
}
private void RegisterCallbacks()
{
if (enableAllButton != null)
{
enableAllButton.AddToClassList("tool-action-button");
enableAllButton.style.marginRight = 4;
enableAllButton.clicked += () => SetAllResourcesState(true);
}
if (disableAllButton != null)
{
disableAllButton.AddToClassList("tool-action-button");
disableAllButton.style.marginRight = 4;
disableAllButton.clicked += () => SetAllResourcesState(false);
}
if (rescanButton != null)
{
rescanButton.AddToClassList("tool-action-button");
rescanButton.clicked += () =>
{
McpLog.Info("Rescanning MCP resources from the editor window.");
MCPServiceLocator.ResourceDiscovery.InvalidateCache();
Refresh();
};
}
}
/// <summary>
/// Rebuilds the resource list and synchronises toggle states.
/// </summary>
public void Refresh()
{
resourceToggleMap.Clear();
categoryContainer?.Clear();
var service = MCPServiceLocator.ResourceDiscovery;
allResources = service.DiscoverAllResources()
.OrderBy(r => r.IsBuiltIn ? 0 : 1)
.ThenBy(r => r.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
bool hasResources = allResources.Count > 0;
enableAllButton?.SetEnabled(hasResources);
disableAllButton?.SetEnabled(hasResources);
if (noteLabel != null)
{
noteLabel.style.display = hasResources ? DisplayStyle.Flex : DisplayStyle.None;
}
if (!hasResources)
{
AddInfoLabel("No MCP resources found. Add classes decorated with [McpForUnityResource] to expose resources.");
UpdateSummary();
return;
}
BuildCategory("Built-in Resources", "built-in", allResources.Where(r => r.IsBuiltIn));
var customResources = allResources.Where(r => !r.IsBuiltIn).ToList();
if (customResources.Count > 0)
{
BuildCategory("Custom Resources", "custom", customResources);
}
else
{
AddInfoLabel("No custom resources detected in loaded assemblies.");
}
UpdateSummary();
}
private void BuildCategory(string title, string prefsSuffix, IEnumerable<ResourceMetadata> resources)
{
var resourceList = resources.ToList();
if (resourceList.Count == 0)
{
return;
}
var foldout = new Foldout
{
text = $"{title} ({resourceList.Count})",
value = EditorPrefs.GetBool(EditorPrefKeys.ResourceFoldoutStatePrefix + prefsSuffix, true)
};
foldout.RegisterValueChangedCallback(evt =>
{
EditorPrefs.SetBool(EditorPrefKeys.ResourceFoldoutStatePrefix + prefsSuffix, evt.newValue);
});
foreach (var resource in resourceList)
{
foldout.Add(CreateResourceRow(resource));
}
categoryContainer?.Add(foldout);
}
private VisualElement CreateResourceRow(ResourceMetadata resource)
{
var row = new VisualElement();
row.AddToClassList("tool-item");
var header = new VisualElement();
header.AddToClassList("tool-item-header");
var toggle = new Toggle(resource.Name)
{
value = MCPServiceLocator.ResourceDiscovery.IsResourceEnabled(resource.Name)
};
toggle.AddToClassList("tool-item-toggle");
toggle.tooltip = string.IsNullOrWhiteSpace(resource.Description) ? resource.Name : resource.Description;
toggle.RegisterValueChangedCallback(evt =>
{
HandleToggleChange(resource, evt.newValue);
});
resourceToggleMap[resource.Name] = toggle;
header.Add(toggle);
var tagsContainer = new VisualElement();
tagsContainer.AddToClassList("tool-tags");
tagsContainer.Add(CreateTag(resource.IsBuiltIn ? "Built-in" : "Custom"));
header.Add(tagsContainer);
row.Add(header);
if (!string.IsNullOrWhiteSpace(resource.Description) && !resource.Description.StartsWith("Resource:", StringComparison.Ordinal))
{
var description = new Label(resource.Description);
description.AddToClassList("tool-item-description");
row.Add(description);
}
return row;
}
private void HandleToggleChange(ResourceMetadata resource, bool enabled, bool updateSummary = true)
{
MCPServiceLocator.ResourceDiscovery.SetResourceEnabled(resource.Name, enabled);
if (updateSummary)
{
UpdateSummary();
}
}
private void SetAllResourcesState(bool enabled)
{
foreach (var resource in allResources)
{
if (!resourceToggleMap.TryGetValue(resource.Name, out var toggle))
{
MCPServiceLocator.ResourceDiscovery.SetResourceEnabled(resource.Name, enabled);
continue;
}
if (toggle.value == enabled)
{
continue;
}
toggle.SetValueWithoutNotify(enabled);
HandleToggleChange(resource, enabled, updateSummary: false);
}
UpdateSummary();
}
private void UpdateSummary()
{
if (summaryLabel == null)
{
return;
}
if (allResources.Count == 0)
{
summaryLabel.text = "No MCP resources discovered.";
return;
}
int enabledCount = allResources.Count(r => MCPServiceLocator.ResourceDiscovery.IsResourceEnabled(r.Name));
summaryLabel.text = $"{enabledCount} of {allResources.Count} resources enabled.";
}
private void AddInfoLabel(string message)
{
var label = new Label(message);
label.AddToClassList("help-text");
categoryContainer?.Add(label);
}
private static Label CreateTag(string text)
{
var tag = new Label(text);
tag.AddToClassList("tool-tag");
return tag;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 465cdffd91f9d461caf4298ca322e3ab
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,15 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="True">
<ui:VisualElement name="resources-section" class="section">
<ui:Label text="Resources" class="section-title" />
<ui:VisualElement class="section-content">
<ui:Label name="resources-summary" class="help-text" text="Discovering resources..." />
<ui:VisualElement name="resources-actions" class="tool-actions">
<ui:Button name="enable-all-resources-button" text="Enable All" class="tool-action-button" />
<ui:Button name="disable-all-resources-button" text="Disable All" class="tool-action-button" />
<ui:Button name="rescan-resources-button" text="Rescan" class="tool-action-button" />
</ui:VisualElement>
<ui:Label name="resources-note" class="help-text" text="Changes apply after reconnecting or re-registering resources." />
<ui:VisualElement name="resource-category-container" class="tool-category-container" />
</ui:VisualElement>
</ui:VisualElement>
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: b455fadaaad0a43c4bae9f3fe784c5c3
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c2f853b1b3974f829a2cc09d52d3d7ad
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,334 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Services;
using MCPForUnity.Editor.Tools;
using UnityEditor;
using UnityEngine.UIElements;
namespace MCPForUnity.Editor.Windows.Components.Tools
{
/// <summary>
/// Controller for the Tools section inside the MCP For Unity editor window.
/// Provides discovery, filtering, and per-tool enablement toggles.
/// </summary>
public class McpToolsSection
{
private readonly Dictionary<string, Toggle> toolToggleMap = new();
private Toggle projectScopedToolsToggle;
private Label summaryLabel;
private Label noteLabel;
private Button enableAllButton;
private Button disableAllButton;
private Button rescanButton;
private VisualElement categoryContainer;
private List<ToolMetadata> allTools = new();
public VisualElement Root { get; }
public McpToolsSection(VisualElement root)
{
Root = root;
CacheUIElements();
RegisterCallbacks();
}
private void CacheUIElements()
{
projectScopedToolsToggle = Root.Q<Toggle>("project-scoped-tools-toggle");
summaryLabel = Root.Q<Label>("tools-summary");
noteLabel = Root.Q<Label>("tools-note");
enableAllButton = Root.Q<Button>("enable-all-button");
disableAllButton = Root.Q<Button>("disable-all-button");
rescanButton = Root.Q<Button>("rescan-button");
categoryContainer = Root.Q<VisualElement>("tool-category-container");
}
private void RegisterCallbacks()
{
if (projectScopedToolsToggle != null)
{
projectScopedToolsToggle.value = EditorPrefs.GetBool(
EditorPrefKeys.ProjectScopedToolsLocalHttp,
false
);
projectScopedToolsToggle.tooltip = "When enabled, register project-scoped tools with HTTP Local transport. Allows per-project tool customization.";
projectScopedToolsToggle.RegisterValueChangedCallback(evt =>
{
EditorPrefs.SetBool(EditorPrefKeys.ProjectScopedToolsLocalHttp, evt.newValue);
});
}
if (enableAllButton != null)
{
enableAllButton.AddToClassList("tool-action-button");
enableAllButton.style.marginRight = 4;
enableAllButton.clicked += () => SetAllToolsState(true);
}
if (disableAllButton != null)
{
disableAllButton.AddToClassList("tool-action-button");
disableAllButton.style.marginRight = 4;
disableAllButton.clicked += () => SetAllToolsState(false);
}
if (rescanButton != null)
{
rescanButton.AddToClassList("tool-action-button");
rescanButton.clicked += () =>
{
McpLog.Info("Rescanning MCP tools from the editor window.");
MCPServiceLocator.ToolDiscovery.InvalidateCache();
Refresh();
};
}
}
/// <summary>
/// Rebuilds the tool list and synchronises toggle states.
/// </summary>
public void Refresh()
{
toolToggleMap.Clear();
categoryContainer?.Clear();
var service = MCPServiceLocator.ToolDiscovery;
allTools = service.DiscoverAllTools()
.OrderBy(tool => IsBuiltIn(tool) ? 0 : 1)
.ThenBy(tool => tool.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
bool hasTools = allTools.Count > 0;
enableAllButton?.SetEnabled(hasTools);
disableAllButton?.SetEnabled(hasTools);
if (noteLabel != null)
{
noteLabel.style.display = hasTools ? DisplayStyle.Flex : DisplayStyle.None;
}
if (!hasTools)
{
AddInfoLabel("No MCP tools found. Add classes decorated with [McpForUnityTool] to expose tools.");
UpdateSummary();
return;
}
BuildCategory("Built-in Tools", "built-in", allTools.Where(IsBuiltIn));
var customTools = allTools.Where(tool => !IsBuiltIn(tool)).ToList();
if (customTools.Count > 0)
{
BuildCategory("Custom Tools", "custom", customTools);
}
else
{
AddInfoLabel("No custom tools detected in loaded assemblies.");
}
UpdateSummary();
}
private void BuildCategory(string title, string prefsSuffix, IEnumerable<ToolMetadata> tools)
{
var toolList = tools.ToList();
if (toolList.Count == 0)
{
return;
}
var foldout = new Foldout
{
text = $"{title} ({toolList.Count})",
value = EditorPrefs.GetBool(EditorPrefKeys.ToolFoldoutStatePrefix + prefsSuffix, true)
};
foldout.RegisterValueChangedCallback(evt =>
{
EditorPrefs.SetBool(EditorPrefKeys.ToolFoldoutStatePrefix + prefsSuffix, evt.newValue);
});
foreach (var tool in toolList)
{
foldout.Add(CreateToolRow(tool));
}
categoryContainer?.Add(foldout);
}
private VisualElement CreateToolRow(ToolMetadata tool)
{
var row = new VisualElement();
row.AddToClassList("tool-item");
var header = new VisualElement();
header.AddToClassList("tool-item-header");
var toggle = new Toggle(tool.Name)
{
value = MCPServiceLocator.ToolDiscovery.IsToolEnabled(tool.Name)
};
toggle.AddToClassList("tool-item-toggle");
toggle.tooltip = string.IsNullOrWhiteSpace(tool.Description) ? tool.Name : tool.Description;
toggle.RegisterValueChangedCallback(evt =>
{
HandleToggleChange(tool, evt.newValue);
});
toolToggleMap[tool.Name] = toggle;
header.Add(toggle);
var tagsContainer = new VisualElement();
tagsContainer.AddToClassList("tool-tags");
bool defaultEnabled = tool.AutoRegister || tool.IsBuiltIn;
tagsContainer.Add(CreateTag(defaultEnabled ? "On by default" : "Off by default"));
tagsContainer.Add(CreateTag(tool.StructuredOutput ? "Structured output" : "Free-form"));
if (tool.RequiresPolling)
{
tagsContainer.Add(CreateTag($"Polling: {tool.PollAction}"));
}
header.Add(tagsContainer);
row.Add(header);
if (!string.IsNullOrWhiteSpace(tool.Description))
{
var description = new Label(tool.Description);
description.AddToClassList("tool-item-description");
row.Add(description);
}
if (tool.Parameters != null && tool.Parameters.Count > 0)
{
var paramSummary = string.Join(", ", tool.Parameters.Select(p =>
$"{p.Name}{(p.Required ? string.Empty : " (optional)")}: {p.Type}"));
var parametersLabel = new Label(paramSummary);
parametersLabel.AddToClassList("tool-parameters");
row.Add(parametersLabel);
}
if (IsManageSceneTool(tool))
{
row.Add(CreateManageSceneActions());
}
return row;
}
private void HandleToggleChange(ToolMetadata tool, bool enabled, bool updateSummary = true)
{
MCPServiceLocator.ToolDiscovery.SetToolEnabled(tool.Name, enabled);
if (updateSummary)
{
UpdateSummary();
}
}
private void SetAllToolsState(bool enabled)
{
foreach (var tool in allTools)
{
if (!toolToggleMap.TryGetValue(tool.Name, out var toggle))
{
MCPServiceLocator.ToolDiscovery.SetToolEnabled(tool.Name, enabled);
continue;
}
if (toggle.value == enabled)
{
continue;
}
toggle.SetValueWithoutNotify(enabled);
HandleToggleChange(tool, enabled, updateSummary: false);
}
UpdateSummary();
}
private void UpdateSummary()
{
if (summaryLabel == null)
{
return;
}
if (allTools.Count == 0)
{
summaryLabel.text = "No MCP tools discovered.";
return;
}
int enabledCount = allTools.Count(tool => MCPServiceLocator.ToolDiscovery.IsToolEnabled(tool.Name));
summaryLabel.text = $"{enabledCount} of {allTools.Count} tools will register with connected clients.";
}
private void AddInfoLabel(string message)
{
var label = new Label(message);
label.AddToClassList("help-text");
categoryContainer?.Add(label);
}
private VisualElement CreateManageSceneActions()
{
var actions = new VisualElement();
actions.AddToClassList("tool-item-actions");
var screenshotButton = new Button(OnManageSceneScreenshotClicked)
{
text = "Capture Screenshot"
};
screenshotButton.AddToClassList("tool-action-button");
screenshotButton.style.marginTop = 4;
screenshotButton.tooltip = "Capture a screenshot to Assets/Screenshots via manage_scene.";
actions.Add(screenshotButton);
return actions;
}
private void OnManageSceneScreenshotClicked()
{
try
{
var response = ManageScene.ExecuteScreenshot();
if (response is SuccessResponse success && !string.IsNullOrWhiteSpace(success.Message))
{
McpLog.Info(success.Message);
}
else if (response is ErrorResponse error && !string.IsNullOrWhiteSpace(error.Error))
{
McpLog.Error(error.Error);
}
else
{
McpLog.Info("Screenshot capture requested.");
}
}
catch (Exception ex)
{
McpLog.Error($"Failed to capture screenshot: {ex.Message}");
}
}
private static Label CreateTag(string text)
{
var tag = new Label(text);
tag.AddToClassList("tool-tag");
return tag;
}
private static bool IsManageSceneTool(ToolMetadata tool) => string.Equals(tool?.Name, "manage_scene", StringComparison.OrdinalIgnoreCase);
private static bool IsBuiltIn(ToolMetadata tool) => tool?.IsBuiltIn ?? false;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c6b3eaf7efb642e89b9b9548458f72d6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,19 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="True">
<ui:VisualElement name="tools-section" class="section">
<ui:Label text="Tools" class="section-title" />
<ui:VisualElement class="section-content">
<ui:VisualElement class="setting-row" name="project-scoped-tools-row">
<ui:Label text="Project-Scoped Tools:" class="setting-label" />
<ui:Toggle name="project-scoped-tools-toggle" />
</ui:VisualElement>
<ui:Label name="tools-summary" class="help-text" text="Discovering tools..." />
<ui:VisualElement name="tools-actions" class="tool-actions">
<ui:Button name="enable-all-button" text="Enable All" class="tool-action-button" />
<ui:Button name="disable-all-button" text="Disable All" class="tool-action-button" />
<ui:Button name="rescan-button" text="Rescan" class="tool-action-button" />
</ui:VisualElement>
<ui:Label name="tools-note" class="help-text" text="Changes apply after reconnecting or re-registering tools." />
<ui:VisualElement name="tool-category-container" class="tool-category-container" />
</ui:VisualElement>
</ui:VisualElement>
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 5a94c7afd72c4dcf9f8a611d85c9a1e4
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f68f3b0ff9e214244ad7e57b106d5c60
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,79 @@
using System;
using MCPForUnity.Editor.Constants;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace MCPForUnity.Editor.Windows.Components.Validation
{
/// <summary>
/// Controller for the Script Validation section.
/// Handles script validation level settings.
/// </summary>
public class McpValidationSection
{
// UI Elements
private EnumField validationLevelField;
private Label validationDescription;
// Data
private ValidationLevel currentValidationLevel = ValidationLevel.Standard;
// Validation levels
public enum ValidationLevel
{
Basic,
Standard,
Comprehensive,
Strict
}
public VisualElement Root { get; private set; }
public McpValidationSection(VisualElement root)
{
Root = root;
CacheUIElements();
InitializeUI();
RegisterCallbacks();
}
private void CacheUIElements()
{
validationLevelField = Root.Q<EnumField>("validation-level");
validationDescription = Root.Q<Label>("validation-description");
}
private void InitializeUI()
{
validationLevelField.Init(ValidationLevel.Standard);
int savedLevel = EditorPrefs.GetInt(EditorPrefKeys.ValidationLevel, 1);
currentValidationLevel = (ValidationLevel)Mathf.Clamp(savedLevel, 0, 3);
validationLevelField.value = currentValidationLevel;
UpdateValidationDescription();
}
private void RegisterCallbacks()
{
validationLevelField.RegisterValueChangedCallback(evt =>
{
currentValidationLevel = (ValidationLevel)evt.newValue;
EditorPrefs.SetInt(EditorPrefKeys.ValidationLevel, (int)currentValidationLevel);
UpdateValidationDescription();
});
}
private void UpdateValidationDescription()
{
validationDescription.text = currentValidationLevel switch
{
ValidationLevel.Basic => "Basic: Validates syntax only. Fast compilation checks.",
ValidationLevel.Standard => "Standard (Recommended): Checks syntax + common errors. Balanced speed and coverage.",
ValidationLevel.Comprehensive => "Comprehensive: Detailed validation including code quality. Slower but thorough.",
ValidationLevel.Strict => "Strict: Maximum validation + warnings as errors. Slowest but catches all issues.",
_ => "Unknown validation level"
};
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c65b5fd2ed3efbf469bbc0a089f845e3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,13 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="True">
<Style src="../Common.uss" />
<ui:VisualElement name="validation-section" class="section">
<ui:Label text="Script Validation" class="section-title" />
<ui:VisualElement class="section-content">
<ui:VisualElement class="setting-column">
<ui:Label text="Validation Level:" class="setting-label" />
<uie:EnumField name="validation-level" class="setting-dropdown" />
<ui:Label name="validation-description" class="validation-description" />
</ui:VisualElement>
</ui:VisualElement>
</ui:VisualElement>
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 3f682815a83bb6841ac61f7f399d903c
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: acc0b0b106a5e4826ab3fdbda7916eaf
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,21 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="True">
<ui:VisualElement class="pref-item">
<ui:VisualElement class="pref-row">
<!-- Key and Known indicator -->
<ui:VisualElement class="key-section">
<ui:Label name="key-label" class="key-label" />
</ui:VisualElement>
<!-- Value field -->
<ui:TextField name="value-field" class="value-field" />
<!-- Type dropdown -->
<ui:DropdownField name="type-dropdown" class="type-dropdown" choices="String,Int,Float,Bool" index="0" />
<!-- Action buttons -->
<ui:VisualElement class="action-buttons">
<ui:Button name="save-button" text="✓" class="save-button" tooltip="Save changes" />
</ui:VisualElement>
</ui:VisualElement>
</ui:VisualElement>
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 06c99d6b0c7fa4fd3842e4d3b2a7407f
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,404 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Helpers;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace MCPForUnity.Editor.Windows
{
/// <summary>
/// Editor window for managing Unity EditorPrefs, specifically for MCP For Unity development
/// </summary>
public class EditorPrefsWindow : EditorWindow
{
// UI Elements
private ScrollView scrollView;
private VisualElement prefsContainer;
private TextField searchField;
private string searchFilter = "";
// Data
private List<EditorPrefItem> currentPrefs = new List<EditorPrefItem>();
private HashSet<string> knownMcpKeys = new HashSet<string>();
// Type mapping for known EditorPrefs
private readonly Dictionary<string, EditorPrefType> knownPrefTypes = new Dictionary<string, EditorPrefType>
{
// Boolean prefs
{ EditorPrefKeys.DebugLogs, EditorPrefType.Bool },
{ EditorPrefKeys.UseHttpTransport, EditorPrefType.Bool },
{ EditorPrefKeys.ResumeHttpAfterReload, EditorPrefType.Bool },
{ EditorPrefKeys.ResumeStdioAfterReload, EditorPrefType.Bool },
{ EditorPrefKeys.UseEmbeddedServer, EditorPrefType.Bool },
{ EditorPrefKeys.LockCursorConfig, EditorPrefType.Bool },
{ EditorPrefKeys.AutoRegisterEnabled, EditorPrefType.Bool },
{ EditorPrefKeys.SetupCompleted, EditorPrefType.Bool },
{ EditorPrefKeys.SetupDismissed, EditorPrefType.Bool },
{ EditorPrefKeys.CustomToolRegistrationEnabled, EditorPrefType.Bool },
{ EditorPrefKeys.TelemetryDisabled, EditorPrefType.Bool },
{ EditorPrefKeys.DevModeForceServerRefresh, EditorPrefType.Bool },
{ EditorPrefKeys.UseBetaServer, EditorPrefType.Bool },
{ EditorPrefKeys.ProjectScopedToolsLocalHttp, EditorPrefType.Bool },
// Integer prefs
{ EditorPrefKeys.UnitySocketPort, EditorPrefType.Int },
{ EditorPrefKeys.ValidationLevel, EditorPrefType.Int },
{ EditorPrefKeys.LastUpdateCheck, EditorPrefType.String },
{ EditorPrefKeys.LastStdIoUpgradeVersion, EditorPrefType.Int },
{ EditorPrefKeys.LastLocalHttpServerPid, EditorPrefType.Int },
{ EditorPrefKeys.LastLocalHttpServerPort, EditorPrefType.Int },
// String prefs
{ EditorPrefKeys.EditorWindowActivePanel, EditorPrefType.String },
{ EditorPrefKeys.ClaudeCliPathOverride, EditorPrefType.String },
{ EditorPrefKeys.UvxPathOverride, EditorPrefType.String },
{ EditorPrefKeys.HttpBaseUrl, EditorPrefType.String },
{ EditorPrefKeys.HttpRemoteBaseUrl, EditorPrefType.String },
{ EditorPrefKeys.HttpTransportScope, EditorPrefType.String },
{ EditorPrefKeys.SessionId, EditorPrefType.String },
{ EditorPrefKeys.WebSocketUrlOverride, EditorPrefType.String },
{ EditorPrefKeys.GitUrlOverride, EditorPrefType.String },
{ EditorPrefKeys.PackageDeploySourcePath, EditorPrefType.String },
{ EditorPrefKeys.PackageDeployLastBackupPath, EditorPrefType.String },
{ EditorPrefKeys.PackageDeployLastTargetPath, EditorPrefType.String },
{ EditorPrefKeys.PackageDeployLastSourcePath, EditorPrefType.String },
{ EditorPrefKeys.ServerSrc, EditorPrefType.String },
{ EditorPrefKeys.LatestKnownVersion, EditorPrefType.String },
{ EditorPrefKeys.LastAssetStoreUpdateCheck, EditorPrefType.String },
{ EditorPrefKeys.LatestKnownAssetStoreVersion, EditorPrefType.String },
{ EditorPrefKeys.LastLocalHttpServerStartedUtc, EditorPrefType.String },
{ EditorPrefKeys.LastLocalHttpServerPidArgsHash, EditorPrefType.String },
{ EditorPrefKeys.LastLocalHttpServerPidFilePath, EditorPrefType.String },
{ EditorPrefKeys.LastLocalHttpServerInstanceToken, EditorPrefType.String },
};
// Templates
private VisualTreeAsset itemTemplate;
/// <summary>
/// Show the EditorPrefs window
/// </summary>
public static void ShowWindow()
{
var window = GetWindow<EditorPrefsWindow>("EditorPrefs");
window.minSize = new Vector2(600, 400);
window.Show();
}
public void CreateGUI()
{
string basePath = AssetPathUtility.GetMcpPackageRootPath();
// Load UXML
var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
$"{basePath}/Editor/Windows/EditorPrefs/EditorPrefsWindow.uxml"
);
if (visualTree == null)
{
McpLog.Error("Failed to load EditorPrefsWindow.uxml template");
return;
}
// Load item template
itemTemplate = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
$"{basePath}/Editor/Windows/EditorPrefs/EditorPrefItem.uxml"
);
if (itemTemplate == null)
{
McpLog.Error("Failed to load EditorPrefItem.uxml template");
return;
}
visualTree.CloneTree(rootVisualElement);
// Add search bar container at the top
var searchContainer = new VisualElement();
searchContainer.style.flexDirection = FlexDirection.Row;
searchContainer.style.marginTop = 8;
searchContainer.style.marginBottom = 20;
searchContainer.style.marginLeft = 4;
searchContainer.style.marginRight = 4;
searchField = new TextField("Search");
searchField.style.flexGrow = 1;
searchField.style.height = 28;
searchField.style.paddingTop = 2;
searchField.style.paddingBottom = 2;
searchField.labelElement.style.unityFontStyleAndWeight = FontStyle.Bold;
searchField.RegisterValueChangedCallback(evt =>
{
searchFilter = evt.newValue ?? "";
RefreshPrefs();
});
var refreshButton = new Button(RefreshPrefs);
refreshButton.text = "↻";
refreshButton.tooltip = "Refresh prefs";
refreshButton.style.width = 30;
refreshButton.style.height = 28;
refreshButton.style.marginLeft = 6;
refreshButton.style.backgroundColor = new Color(0.9f, 0.5f, 0.1f);
searchContainer.Add(searchField);
searchContainer.Add(refreshButton);
rootVisualElement.Insert(0, searchContainer);
// Get references
scrollView = rootVisualElement.Q<ScrollView>("scroll-view");
prefsContainer = rootVisualElement.Q<VisualElement>("prefs-container");
// Load known MCP keys
LoadKnownMcpKeys();
// Load initial data
RefreshPrefs();
}
private void LoadKnownMcpKeys()
{
knownMcpKeys.Clear();
var fields = typeof(EditorPrefKeys).GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
foreach (var field in fields)
{
if (field.IsLiteral && !field.IsInitOnly)
{
knownMcpKeys.Add(field.GetValue(null).ToString());
}
}
}
private void RefreshPrefs()
{
currentPrefs.Clear();
prefsContainer.Clear();
// Get all EditorPrefs keys
var allKeys = new List<string>();
// Always show all MCP keys
allKeys.AddRange(knownMcpKeys);
// Try to find additional MCP keys
var mcpKeys = GetAllMcpKeys();
foreach (var key in mcpKeys)
{
if (!allKeys.Contains(key))
{
allKeys.Add(key);
}
}
// Sort keys
allKeys.Sort();
// Pre-trim filter once outside the loop
var filter = searchFilter?.Trim();
// Create items for existing prefs
foreach (var key in allKeys)
{
// Skip Customer UUID but show everything else that's defined
if (key != EditorPrefKeys.CustomerUuid)
{
// Apply search filter using OrdinalIgnoreCase for fewer allocations
if (!string.IsNullOrEmpty(filter) &&
key.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0)
{
continue;
}
var item = CreateEditorPrefItem(key);
if (item != null)
{
currentPrefs.Add(item);
prefsContainer.Add(CreateItemUI(item));
}
}
}
}
private List<string> GetAllMcpKeys()
{
// This is a simplified approach - in reality, getting all EditorPrefs is platform-specific
// For now, we'll return known MCP keys that might exist
var keys = new List<string>();
// Add some common MCP keys that might not be in EditorPrefKeys
keys.Add("MCPForUnity.TestKey");
// Filter to only those that actually exist
return keys.Where(EditorPrefs.HasKey).ToList();
}
private EditorPrefItem CreateEditorPrefItem(string key)
{
var item = new EditorPrefItem { Key = key, IsKnown = knownMcpKeys.Contains(key) };
// Check if we know the type of this pref
if (knownPrefTypes.TryGetValue(key, out var knownType))
{
// Use the known type
switch (knownType)
{
case EditorPrefType.Bool:
item.Type = EditorPrefType.Bool;
item.Value = EditorPrefs.GetBool(key, false).ToString();
break;
case EditorPrefType.Int:
item.Type = EditorPrefType.Int;
item.Value = EditorPrefs.GetInt(key, 0).ToString();
break;
case EditorPrefType.Float:
item.Type = EditorPrefType.Float;
item.Value = EditorPrefs.GetFloat(key, 0f).ToString();
break;
case EditorPrefType.String:
item.Type = EditorPrefType.String;
item.Value = EditorPrefs.GetString(key, "");
break;
}
}
else
{
// Only try to detect type for unknown keys that actually exist
if (!EditorPrefs.HasKey(key))
{
// Key doesn't exist and we don't know its type, skip it
return null;
}
// Unknown pref - try to detect type
var stringValue = EditorPrefs.GetString(key, "");
if (int.TryParse(stringValue, out var intValue))
{
item.Type = EditorPrefType.Int;
item.Value = intValue.ToString();
}
else if (float.TryParse(stringValue, out var floatValue))
{
item.Type = EditorPrefType.Float;
item.Value = floatValue.ToString();
}
else if (bool.TryParse(stringValue, out var boolValue))
{
item.Type = EditorPrefType.Bool;
item.Value = boolValue.ToString();
}
else
{
item.Type = EditorPrefType.String;
item.Value = stringValue;
}
}
return item;
}
private VisualElement CreateItemUI(EditorPrefItem item)
{
if (itemTemplate == null)
{
McpLog.Error("Item template not loaded");
return new VisualElement();
}
var itemElement = itemTemplate.CloneTree();
// Set values
itemElement.Q<Label>("key-label").text = item.Key;
var valueField = itemElement.Q<TextField>("value-field");
valueField.value = item.Value;
var typeDropdown = itemElement.Q<DropdownField>("type-dropdown");
typeDropdown.index = (int)item.Type;
// Buttons
var saveButton = itemElement.Q<Button>("save-button");
// Callbacks
saveButton.clicked += () => SavePref(item, valueField.value, (EditorPrefType)typeDropdown.index);
return itemElement;
}
private void SavePref(EditorPrefItem item, string newValue, EditorPrefType newType)
{
SaveValue(item.Key, newValue, newType);
RefreshPrefs();
}
private void SaveValue(string key, string value, EditorPrefType type)
{
switch (type)
{
case EditorPrefType.String:
EditorPrefs.SetString(key, value);
break;
case EditorPrefType.Int:
if (int.TryParse(value, out var intValue))
{
EditorPrefs.SetInt(key, intValue);
}
else
{
EditorUtility.DisplayDialog("Error", $"Cannot convert '{value}' to int", "OK");
return;
}
break;
case EditorPrefType.Float:
if (float.TryParse(value, out var floatValue))
{
EditorPrefs.SetFloat(key, floatValue);
}
else
{
EditorUtility.DisplayDialog("Error", $"Cannot convert '{value}' to float", "OK");
return;
}
break;
case EditorPrefType.Bool:
if (bool.TryParse(value, out var boolValue))
{
EditorPrefs.SetBool(key, boolValue);
}
else
{
EditorUtility.DisplayDialog("Error", $"Cannot convert '{value}' to bool (use 'True' or 'False')", "OK");
return;
}
break;
}
}
}
/// <summary>
/// Represents an EditorPrefs item
/// </summary>
public class EditorPrefItem
{
public string Key { get; set; }
public string Value { get; set; }
public EditorPrefType Type { get; set; }
public bool IsKnown { get; set; }
}
/// <summary>
/// EditorPrefs value types
/// </summary>
public enum EditorPrefType
{
String,
Int,
Float,
Bool
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ceb71bbae267c4765aff72e4cc845ecb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,225 @@
.header {
padding-bottom: 16px;
border-bottom-width: 1px;
border-bottom-color: #333333;
margin-bottom: 16px;
}
.title {
-unity-font-style: bold;
font-size: 18px;
margin-bottom: 4px;
}
.description {
color: #999999;
font-size: 12px;
white-space: normal;
margin-left: 4px;
}
.controls {
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 8px;
background-color: #393939;
border-radius: 4px;
}
.add-section {
margin-bottom: 16px;
}
.primary-button {
-unity-text-align: middle-left;
background-color: #4a90e2;
color: white;
border-radius: 4px;
padding: 8px 16px;
}
.primary-button:hover {
background-color: #357abd;
}
.secondary-button {
background-color: #393939;
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-left-color: #555555;
border-right-color: #555555;
border-top-color: #555555;
border-bottom-color: #555555;
border-radius: 4px;
padding: 6px 12px;
width: 80px;
}
.secondary-button:hover {
background-color: #484848;
}
/* Add New Row */
.add-new-row {
background-color: #393939;
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-left-color: #555555;
border-right-color: #555555;
border-top-color: #555555;
border-bottom-color: #555555;
border-radius: 4px;
padding: 16px;
margin-bottom: 16px;
}
.add-row-content {
flex-direction: row;
align-items: flex-end;
}
.add-row-content .key-field {
flex: 1;
min-width: 200px;
margin-right: 12px;
}
.add-row-content .value-field {
flex: 1;
min-width: 150px;
margin-right: 12px;
}
.add-row-content .type-dropdown {
width: 100px;
margin-right: 12px;
}
.add-buttons {
flex-direction: row;
justify-content: flex-end;
margin-top: 8px;
}
.add-buttons .cancel-button {
margin-left: 8px;
}
.save-button {
background-color: #4caf50;
color: white;
min-width: 60px;
border-radius: 4px;
}
.save-button:hover {
background-color: #45a049;
}
.cancel-button {
background-color: #393939;
border-left-width: 1px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-left-color: #555555;
border-right-color: #555555;
border-top-color: #555555;
border-bottom-color: #555555;
min-width: 60px;
border-radius: 4px;
}
.cancel-button:hover {
background-color: #484848;
}
/* Pref Items */
.prefs-container {
flex-direction: column;
}
.pref-item {
margin-bottom: 0;
background-color: #393939;
border-left-width: 0;
border-right-width: 0;
border-top-width: 0;
border-bottom-width: 1px;
border-bottom-color: #555555;
border-radius: 0;
padding: 8px;
}
.pref-row {
flex-direction: row;
align-items: center;
flex-wrap: nowrap; /* Prevent wrapping */
}
.pref-row .key-section {
flex-shrink: 0;
width: 200px; /* Fixed width for key section */
margin-right: 12px;
}
.pref-row .value-field {
flex: 1;
min-width: 150px;
margin-right: 12px;
}
.pref-row .type-dropdown {
width: 100px;
margin-right: 12px;
}
.pref-row .action-buttons {
flex-direction: row;
width: 32px;
justify-content: flex-start;
}
.key-section {
flex-direction: column;
min-width: 200px;
}
.key-label {
-unity-font-style: bold;
color: white;
white-space: normal;
}
.value-field {
flex: 1;
min-width: 150px;
}
.type-dropdown {
width: 100px;
}
.action-buttons {
flex-direction: row;
}
.action-buttons .save-button {
background-color: #4caf50;
color: white;
min-width: 32px;
height: 28px;
padding: 0;
font-size: 16px;
-unity-font-style: bold;
}
.action-buttons .save-button:hover {
background-color: #45a049;
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3980375ed546e47abafcafe11e953e87
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0

View File

@@ -0,0 +1,30 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="True">
<Style src="../Components/Common.uss" />
<Style src="EditorPrefsWindow.uss" />
<ui:ScrollView name="scroll-view" mode="Vertical">
<!-- Header -->
<ui:VisualElement class="header">
<ui:Label text="EditorPrefs Manager" class="title" />
<ui:Label text="Manage MCP for Unity EditorPrefs. Useful for development and testing." class="description" />
</ui:VisualElement>
<!-- Add New Row (initially hidden) -->
<ui:VisualElement name="add-new-row" class="add-new-row" style="display: none;">
<ui:VisualElement class="add-row-content">
<ui:TextField name="new-key-field" label="Key" class="key-field" />
<ui:TextField name="new-value-field" label="Value" class="value-field" />
<ui:DropdownField name="new-type-dropdown" label="Type" class="type-dropdown" choices="String,Int,Float,Bool" index="0" />
<ui:VisualElement class="add-buttons">
<ui:Button name="create-button" text="Create" class="save-button" />
<ui:Button name="cancel-button" text="Cancel" class="cancel-button" />
</ui:VisualElement>
</ui:VisualElement>
</ui:VisualElement>
<!-- Prefs List -->
<ui:VisualElement name="prefs-container" class="prefs-container">
<!-- Items will be added here programmatically -->
</ui:VisualElement>
</ui:ScrollView>
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 0cfa716884c1445d8a5e9581bbe2e9ce
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,612 @@
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.ClientConfig;
using MCPForUnity.Editor.Windows.Components.Connection;
using MCPForUnity.Editor.Windows.Components.Resources;
using MCPForUnity.Editor.Windows.Components.Tools;
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 McpValidationSection validationSection;
private McpAdvancedSection advancedSection;
private McpToolsSection toolsSection;
private McpResourcesSection resourcesSection;
// UI Elements
private Label versionLabel;
private VisualElement updateNotification;
private Label updateNotificationText;
private ToolbarToggle clientsTabToggle;
private ToolbarToggle validationTabToggle;
private ToolbarToggle advancedTabToggle;
private ToolbarToggle toolsTabToggle;
private ToolbarToggle resourcesTabToggle;
private VisualElement clientsPanel;
private VisualElement validationPanel;
private VisualElement advancedPanel;
private VisualElement toolsPanel;
private VisualElement resourcesPanel;
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 enum ActivePanel
{
Clients,
Validation,
Advanced,
Tools,
Resources
}
internal static void CloseAllWindows()
{
var windows = OpenWindows.Where(window => window != null).ToArray();
foreach (var window in windows)
{
window.Close();
}
}
public static void ShowWindow()
{
var window = GetWindow<MCPForUnityEditorWindow>("MCP For Unity");
window.minSize = new Vector2(500, 340);
}
// 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;
}
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);
}
// 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");
validationPanel = rootVisualElement.Q<VisualElement>("validation-panel");
advancedPanel = rootVisualElement.Q<VisualElement>("advanced-panel");
toolsPanel = rootVisualElement.Q<VisualElement>("tools-panel");
resourcesPanel = rootVisualElement.Q<VisualElement>("resources-panel");
var clientsContainer = rootVisualElement.Q<VisualElement>("clients-container");
var validationContainer = rootVisualElement.Q<VisualElement>("validation-container");
var advancedContainer = rootVisualElement.Q<VisualElement>("advanced-container");
var toolsContainer = rootVisualElement.Q<VisualElement>("tools-container");
var resourcesContainer = rootVisualElement.Q<VisualElement>("resources-container");
if (clientsPanel == null || validationPanel == null || advancedPanel == null || toolsPanel == null || resourcesPanel == 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 (validationContainer == null)
{
McpLog.Error("Failed to find validation-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;
}
// Initialize version label
UpdateVersionLabel(EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, true));
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);
}
// Load and initialize Validation section
var validationTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
$"{basePath}/Editor/Windows/Components/Validation/McpValidationSection.uxml"
);
if (validationTree != null)
{
var validationRoot = validationTree.Instantiate();
validationContainer.Add(validationRoot);
validationSection = new McpValidationSection(validationRoot);
}
// 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();
advancedSection.OnTestConnectionRequested += async () =>
{
if (connectionSection != null)
await connectionSection.VerifyBridgeConnectionAsync();
};
advancedSection.OnBetaModeChanged += UpdateVersionLabel;
// Wire up health status updates from Connection to Advanced
connectionSection?.SetHealthStatusUpdateCallback((isHealthy, statusText) =>
advancedSection?.UpdateHealthStatus(isHealthy, statusText));
}
// 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.");
}
// 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();
}
private void UpdateVersionLabel(bool useBetaServer)
{
if (versionLabel == null)
{
return;
}
string version = AssetPathUtility.GetPackageVersion();
versionLabel.text = useBetaServer ? $"v{version} β" : $"v{version}";
versionLabel.tooltip = useBetaServer
? "Beta server mode - fetching pre-release server versions from PyPI"
: $"MCP For Unity v{version}";
}
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");
validationTabToggle = rootVisualElement.Q<ToolbarToggle>("validation-tab");
advancedTabToggle = rootVisualElement.Q<ToolbarToggle>("advanced-tab");
toolsTabToggle = rootVisualElement.Q<ToolbarToggle>("tools-tab");
resourcesTabToggle = rootVisualElement.Q<ToolbarToggle>("resources-tab");
clientsPanel?.RemoveFromClassList("hidden");
validationPanel?.RemoveFromClassList("hidden");
advancedPanel?.RemoveFromClassList("hidden");
toolsPanel?.RemoveFromClassList("hidden");
resourcesPanel?.RemoveFromClassList("hidden");
if (clientsTabToggle != null)
{
clientsTabToggle.RegisterValueChangedCallback(evt =>
{
if (evt.newValue) SwitchPanel(ActivePanel.Clients);
});
}
if (validationTabToggle != null)
{
validationTabToggle.RegisterValueChangedCallback(evt =>
{
if (evt.newValue) SwitchPanel(ActivePanel.Validation);
});
}
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);
});
}
var savedPanel = EditorPrefs.GetString(EditorPrefKeys.EditorWindowActivePanel, ActivePanel.Clients.ToString());
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 (validationPanel != null)
{
validationPanel.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;
}
// Show selected panel
switch (panel)
{
case ActivePanel.Clients:
if (clientsPanel != null) clientsPanel.style.display = DisplayStyle.Flex;
break;
case ActivePanel.Validation:
if (validationPanel != null) validationPanel.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;
}
// Update toggle states
clientsTabToggle?.SetValueWithoutNotify(panel == ActivePanel.Clients);
validationTabToggle?.SetValueWithoutNotify(panel == ActivePanel.Validation);
advancedTabToggle?.SetValueWithoutNotify(panel == ActivePanel.Advanced);
toolsTabToggle?.SetValueWithoutNotify(panel == ActivePanel.Tools);
resourcesTabToggle?.SetValueWithoutNotify(panel == ActivePanel.Resources);
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}");
}
};
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9f0c6e1d3e4d5e6f7a8b9c0d1e2f3a4d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,133 @@
/* Root Layout */
#root-container {
padding: 0px;
flex-direction: column;
flex-grow: 1;
overflow: hidden;
}
/* Header Bar */
.header-bar {
flex-direction: row;
align-items: center;
justify-content: space-between;
padding: 12px 18px;
margin: 12px 12px 5px 12px;
min-height: 44px;
flex-shrink: 0;
background-color: rgba(0, 0, 0, 0.05);
border-radius: 4px;
border-width: 1px;
border-color: rgba(0, 0, 0, 0.15);
}
.header-title {
font-size: 16px;
-unity-font-style: bold;
letter-spacing: 0.5px;
}
.header-version {
font-size: 11px;
color: rgba(180, 210, 255, 1);
padding: 3px 10px;
background-color: rgba(50, 120, 200, 0.25);
border-radius: 10px;
border-width: 1px;
border-color: rgba(80, 150, 220, 0.4);
}
/* Update Notification */
.update-notification {
display: none;
padding: 8px 16px;
margin: 0px 12px;
background-color: rgba(100, 200, 100, 0.15);
border-radius: 4px;
border-width: 1px;
border-color: rgba(100, 200, 100, 0.3);
}
.update-notification.visible {
display: flex;
}
.update-notification-text {
font-size: 11px;
color: rgba(100, 200, 100, 1);
white-space: normal;
}
/* Tabs */
.tab-toolbar {
margin: 8px 12px 0px 12px;
padding: 0px;
background-color: transparent;
border-width: 0px;
border-bottom-width: 1px;
border-bottom-color: rgba(0, 0, 0, 0.15);
}
.tab-toolbar .unity-toolbar-button {
flex-grow: 1;
min-height: 32px;
font-size: 12px;
border-width: 0px;
background-color: transparent;
margin: 0px 2px 0px 0px;
padding: 0px 12px;
border-radius: 4px 4px 0px 0px;
margin-bottom: -1px;
}
.tab-toolbar .unity-toolbar-button:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.tab-toolbar .unity-toolbar-button:checked {
background-color: rgba(0, 0, 0, 0.04);
border-width: 1px;
border-color: rgba(0, 0, 0, 0.15);
border-bottom-width: 1px;
border-bottom-color: rgba(56, 56, 56, 1);
}
/* Panels */
.panel-scroll {
flex-grow: 1;
margin: 0px;
padding: 0px 8px 0px 8px;
}
.hidden {
display: none;
}
.section-stack {
flex-direction: column;
}
/* Light Theme */
.unity-theme-light .header-bar {
background-color: rgba(0, 0, 0, 0.04);
border-color: rgba(0, 0, 0, 0.15);
}
.unity-theme-light .header-version {
background-color: rgba(0, 0, 0, 0.08);
}
.unity-theme-light .tab-toolbar .unity-toolbar-button:checked {
background-color: rgba(255, 255, 255, 0.5);
border-color: rgba(0, 0, 0, 0.15);
border-bottom-color: rgba(194, 194, 194, 1);
}
.unity-theme-light .update-notification {
background-color: rgba(100, 200, 100, 0.1);
border-color: rgba(100, 200, 100, 0.25);
}
.unity-theme-dark .update-notification-text {
color: rgba(150, 255, 150, 1);
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8f9b5e0c2d3c4e5f6a7b8c9d0e1f2a3c
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0

View File

@@ -0,0 +1,33 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="True">
<ui:VisualElement name="root-container" class="root-layout">
<ui:VisualElement name="header-bar" class="header-bar">
<ui:Label text="MCP For Unity" name="title" class="header-title" />
<ui:Label text="v9.1.0" name="version-label" class="header-version" />
</ui:VisualElement>
<ui:VisualElement name="update-notification" class="update-notification">
<ui:Label name="update-notification-text" class="update-notification-text" />
</ui:VisualElement>
<uie:Toolbar name="tab-toolbar" class="tab-toolbar">
<uie:ToolbarToggle name="clients-tab" text="Connect" value="true" />
<uie:ToolbarToggle name="tools-tab" text="Tools" />
<uie:ToolbarToggle name="resources-tab" text="Resources" />
<uie:ToolbarToggle name="validation-tab" text="Scripts" />
<uie:ToolbarToggle name="advanced-tab" text="Advanced" />
</uie:Toolbar>
<ui:ScrollView name="clients-panel" class="panel-scroll" style="flex-grow: 1;">
<ui:VisualElement name="clients-container" class="section-stack" />
</ui:ScrollView>
<ui:ScrollView name="validation-panel" class="panel-scroll hidden" style="flex-grow: 1;">
<ui:VisualElement name="validation-container" class="section-stack" />
</ui:ScrollView>
<ui:ScrollView name="advanced-panel" class="panel-scroll hidden" style="flex-grow: 1;">
<ui:VisualElement name="advanced-container" class="section-stack" />
</ui:ScrollView>
<ui:ScrollView name="tools-panel" class="panel-scroll hidden" style="flex-grow: 1;">
<ui:VisualElement name="tools-container" class="section-stack" />
</ui:ScrollView>
<ui:ScrollView name="resources-panel" class="panel-scroll hidden" style="flex-grow: 1;">
<ui:VisualElement name="resources-container" class="section-stack" />
</ui:ScrollView>
</ui:VisualElement>
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: 7f8a4e9c1d2b3e4f5a6b7c8d9e0f1a2b
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}

View File

@@ -0,0 +1,170 @@
using System;
using MCPForUnity.Editor.Dependencies;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Helpers;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace MCPForUnity.Editor.Windows
{
/// <summary>
/// Setup window for checking and guiding dependency installation
/// </summary>
public class MCPSetupWindow : EditorWindow
{
// UI Elements
private VisualElement pythonIndicator;
private Label pythonVersion;
private Label pythonDetails;
private VisualElement uvIndicator;
private Label uvVersion;
private Label uvDetails;
private Label statusMessage;
private VisualElement installationSection;
private Label installationInstructions;
private Button openPythonLinkButton;
private Button openUvLinkButton;
private Button refreshButton;
private Button doneButton;
private DependencyCheckResult _dependencyResult;
public static void ShowWindow(DependencyCheckResult dependencyResult = null)
{
var window = GetWindow<MCPSetupWindow>("MCP Setup");
window.minSize = new Vector2(480, 320);
window._dependencyResult = dependencyResult ?? DependencyManager.CheckAllDependencies();
window.Show();
}
public void CreateGUI()
{
string basePath = AssetPathUtility.GetMcpPackageRootPath();
// Load UXML
var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(
$"{basePath}/Editor/Windows/MCPSetupWindow.uxml"
);
if (visualTree == null)
{
McpLog.Error($"Failed to load UXML at: {basePath}/Editor/Windows/MCPSetupWindow.uxml");
return;
}
visualTree.CloneTree(rootVisualElement);
// Cache UI elements
pythonIndicator = rootVisualElement.Q<VisualElement>("python-indicator");
pythonVersion = rootVisualElement.Q<Label>("python-version");
pythonDetails = rootVisualElement.Q<Label>("python-details");
uvIndicator = rootVisualElement.Q<VisualElement>("uv-indicator");
uvVersion = rootVisualElement.Q<Label>("uv-version");
uvDetails = rootVisualElement.Q<Label>("uv-details");
statusMessage = rootVisualElement.Q<Label>("status-message");
installationSection = rootVisualElement.Q<VisualElement>("installation-section");
installationInstructions = rootVisualElement.Q<Label>("installation-instructions");
openPythonLinkButton = rootVisualElement.Q<Button>("open-python-link-button");
openUvLinkButton = rootVisualElement.Q<Button>("open-uv-link-button");
refreshButton = rootVisualElement.Q<Button>("refresh-button");
doneButton = rootVisualElement.Q<Button>("done-button");
// Register callbacks
refreshButton.clicked += OnRefreshClicked;
doneButton.clicked += OnDoneClicked;
openPythonLinkButton.clicked += OnOpenPythonInstallClicked;
openUvLinkButton.clicked += OnOpenUvInstallClicked;
// Initial update
UpdateUI();
}
private void OnEnable()
{
if (_dependencyResult == null)
{
_dependencyResult = DependencyManager.CheckAllDependencies();
}
}
private void OnRefreshClicked()
{
_dependencyResult = DependencyManager.CheckAllDependencies();
UpdateUI();
}
private void OnDoneClicked()
{
Setup.SetupWindowService.MarkSetupCompleted();
Close();
}
private void OnOpenPythonInstallClicked()
{
var (pythonUrl, _) = DependencyManager.GetInstallationUrls();
Application.OpenURL(pythonUrl);
}
private void OnOpenUvInstallClicked()
{
var (_, uvUrl) = DependencyManager.GetInstallationUrls();
Application.OpenURL(uvUrl);
}
private void UpdateUI()
{
if (_dependencyResult == null)
return;
// Update Python status
var pythonDep = _dependencyResult.Dependencies.Find(d => d.Name == "Python");
if (pythonDep != null)
{
UpdateDependencyStatus(pythonIndicator, pythonVersion, pythonDetails, pythonDep);
}
// Update uv status
var uvDep = _dependencyResult.Dependencies.Find(d => d.Name == "uv Package Manager");
if (uvDep != null)
{
UpdateDependencyStatus(uvIndicator, uvVersion, uvDetails, uvDep);
}
// Update overall status
if (_dependencyResult.IsSystemReady)
{
statusMessage.text = "✓ All requirements met! MCP for Unity is ready to use.";
statusMessage.style.color = new StyleColor(Color.green);
installationSection.style.display = DisplayStyle.None;
}
else
{
statusMessage.text = "⚠ Missing dependencies. MCP for Unity requires all dependencies to function.";
statusMessage.style.color = new StyleColor(new Color(1f, 0.6f, 0f)); // Orange
installationSection.style.display = DisplayStyle.Flex;
installationInstructions.text = DependencyManager.GetInstallationRecommendations();
}
}
private void UpdateDependencyStatus(VisualElement indicator, Label versionLabel, Label detailsLabel, DependencyStatus dep)
{
if (dep.IsAvailable)
{
indicator.RemoveFromClassList("invalid");
indicator.AddToClassList("valid");
versionLabel.text = $"v{dep.Version}";
detailsLabel.text = dep.Details ?? "Available";
detailsLabel.style.color = new StyleColor(Color.gray);
}
else
{
indicator.RemoveFromClassList("valid");
indicator.AddToClassList("invalid");
versionLabel.text = "Not Found";
detailsLabel.text = dep.ErrorMessage ?? "Not available";
detailsLabel.style.color = new StyleColor(Color.red);
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 62a298f9ec603ba489eaab14b97f1cea
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,106 @@
/* MCP Setup Window Styles */
/* Root container */
#root-container {
padding: 20px;
flex-grow: 1;
}
/* Dependency list */
#dependency-list {
margin-top: 8px;
}
/* Dependency item container */
.dependency-item {
margin-bottom: 12px;
}
/* Dependency row (name + version + indicator) */
.dependency-row {
flex-direction: row;
align-items: center;
}
/* Dependency name label */
.dependency-name {
-unity-font-style: bold;
min-width: 150px;
}
/* Status indicator positioning */
.dependency-row .status-indicator-small {
margin-left: 8px;
}
/* Dependency details text */
.dependency-details {
margin-left: 0px;
margin-top: 4px;
}
/* Status message container */
#status-message-container {
margin-top: 16px;
}
/* Status message text */
#status-message {
white-space: normal;
}
/* Installation section */
#installation-section {
margin-top: 16px;
padding: 12px;
background-color: rgba(0, 0, 0, 0.05);
border-radius: 6px;
border-width: 1px;
border-color: rgba(0, 0, 0, 0.2);
display: none;
}
.installation-container {
flex-shrink: 1;
min-width: 0;
}
/* Installation section title */
.installation-title {
-unity-font-style: bold;
margin-bottom: 8px;
}
/* Installation instructions text */
#installation-instructions {
white-space: normal;
margin-bottom: 4px;
}
.install-links-row {
flex-direction: row;
flex-wrap: wrap;
}
.install-link-button {
flex-grow: 1;
min-width: 180px;
margin-top: 0;
}
/* Button container at bottom */
.button-container {
flex-direction: row;
align-items: center;
justify-content: flex-end;
}
/* Button sizing */
.setup-button {
width: 96px;
}
/* Description text spacing */
.description-text {
margin-bottom: 12px;
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b4426760e34ff484a8ed955e588b570b
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0

View File

@@ -0,0 +1,58 @@
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="True">
<Style src="Components/Common.uss" />
<Style src="MCPSetupWindow.uss" />
<ui:ScrollView name="root-container" mode="Vertical">
<ui:Label text="MCP for Unity Setup" name="title" class="title" />
<ui:VisualElement class="section">
<ui:Label text="System Requirements" class="section-title" />
<ui:VisualElement class="section-content">
<ui:Label text="MCP for Unity requires Python 3.10+ and UV package manager to function." class="help-text description-text" />
<!-- Dependency Status -->
<ui:VisualElement name="dependency-list">
<!-- Python Status -->
<ui:VisualElement class="dependency-item">
<ui:VisualElement class="dependency-row">
<ui:Label text="Python" class="dependency-name" />
<ui:Label name="python-version" text="..." class="setting-value" />
<ui:VisualElement name="python-indicator" class="status-indicator-small" />
</ui:VisualElement>
<ui:Label name="python-details" class="help-text dependency-details" />
</ui:VisualElement>
<!-- UV Status -->
<ui:VisualElement class="dependency-item">
<ui:VisualElement class="dependency-row">
<ui:Label text="UV Package Manager" class="dependency-name" />
<ui:Label name="uv-version" text="..." class="setting-value" />
<ui:VisualElement name="uv-indicator" class="status-indicator-small" />
</ui:VisualElement>
<ui:Label name="uv-details" class="help-text dependency-details" />
</ui:VisualElement>
</ui:VisualElement>
<!-- Overall Status Message -->
<ui:VisualElement name="status-message-container">
<ui:Label name="status-message" class="help-text" />
</ui:VisualElement>
<!-- Installation Instructions (shown when dependencies missing) -->
<ui:VisualElement name="installation-section" class="installation-container">
<ui:Label text="Installation Instructions" class="installation-title" />
<ui:Label name="installation-instructions" class="help-text" />
<ui:VisualElement class="install-links-row">
<ui:Button name="open-python-link-button" text="Open Python Install Page" class="secondary-button install-link-button" />
<ui:Button name="open-uv-link-button" text="Open UV Install Page" class="secondary-button install-link-button" />
</ui:VisualElement>
</ui:VisualElement>
</ui:VisualElement>
</ui:VisualElement>
<!-- Action Buttons -->
<ui:VisualElement class="button-container">
<ui:Button name="refresh-button" text="Refresh" class="setup-button secondary-button" />
<ui:Button name="done-button" text="Done" class="setup-button action-button" />
</ui:VisualElement>
</ui:ScrollView>
</ui:UXML>

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: bf9567b4c9d76a14e9476c2d47c4b017
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}