chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 82074be914aefa84cb557c599d2319b3
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7723ed5eaaccb104e93acb9fd2d8cd32
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,680 @@
|
||||
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 autoStartOnLoadToggle;
|
||||
private Toggle debugLogsToggle;
|
||||
private Toggle logRecordToggle;
|
||||
private Toggle devModeForceRefreshToggle;
|
||||
private Toggle allowLanHttpBindToggle;
|
||||
private Toggle allowInsecureRemoteHttpToggle;
|
||||
private TextField screenshotsFolderOverride;
|
||||
private Button browseScreenshotsFolderButton;
|
||||
private Button clearScreenshotsFolderButton;
|
||||
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 OnPackageDeployed;
|
||||
|
||||
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");
|
||||
autoStartOnLoadToggle = Root.Q<Toggle>("auto-start-on-load-toggle");
|
||||
debugLogsToggle = Root.Q<Toggle>("debug-logs-toggle");
|
||||
logRecordToggle = Root.Q<Toggle>("log-record-toggle");
|
||||
devModeForceRefreshToggle = Root.Q<Toggle>("dev-mode-force-refresh-toggle");
|
||||
allowLanHttpBindToggle = Root.Q<Toggle>("allow-lan-http-bind-toggle");
|
||||
allowInsecureRemoteHttpToggle = Root.Q<Toggle>("allow-insecure-remote-http-toggle");
|
||||
screenshotsFolderOverride = Root.Q<TextField>("screenshots-folder-override");
|
||||
browseScreenshotsFolderButton = Root.Q<Button>("browse-screenshots-folder-button");
|
||||
clearScreenshotsFolderButton = Root.Q<Button>("clear-screenshots-folder-button");
|
||||
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 (logRecordToggle != null)
|
||||
{
|
||||
logRecordToggle.tooltip = "Log every MCP tool execution (tool, action, status, duration) to Assets/UnityMCP/Log/mcp.log.";
|
||||
var logRecordLabel = logRecordToggle?.parent?.Q<Label>();
|
||||
if (logRecordLabel != null)
|
||||
logRecordLabel.tooltip = logRecordToggle.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 (allowLanHttpBindToggle != null)
|
||||
{
|
||||
allowLanHttpBindToggle.tooltip = "Allow HTTP Local to bind on all interfaces (0.0.0.0 / ::). Disabled by default because devices on your LAN may reach MCP tools.";
|
||||
var lanBindLabel = allowLanHttpBindToggle?.parent?.Q<Label>();
|
||||
if (lanBindLabel != null)
|
||||
lanBindLabel.tooltip = allowLanHttpBindToggle.tooltip;
|
||||
}
|
||||
if (allowInsecureRemoteHttpToggle != null)
|
||||
{
|
||||
allowInsecureRemoteHttpToggle.tooltip = "Allow HTTP Remote over plaintext http/ws. Disabled by default to require HTTPS/WSS.";
|
||||
var insecureRemoteLabel = allowInsecureRemoteHttpToggle?.parent?.Q<Label>();
|
||||
if (insecureRemoteLabel != null)
|
||||
insecureRemoteLabel.tooltip = allowInsecureRemoteHttpToggle.tooltip;
|
||||
}
|
||||
if (testConnectionButton != null)
|
||||
testConnectionButton.tooltip = "Test the connection between Unity and the MCP server.";
|
||||
if (screenshotsFolderOverride != null)
|
||||
{
|
||||
screenshotsFolderOverride.tooltip = "Default folder for screenshots from manage_camera / manage_ui. " +
|
||||
"Project-relative (e.g. 'Assets/Screenshots' or 'Captures'). Empty = built-in default (Assets/Screenshots). " +
|
||||
"Per-call 'output_folder' parameters always override this.";
|
||||
screenshotsFolderOverride.SetValueWithoutNotify(ScreenshotPreferences.DefaultFolder);
|
||||
}
|
||||
if (browseScreenshotsFolderButton != null)
|
||||
browseScreenshotsFolderButton.tooltip = "Pick a folder inside the project; the path is stored project-relative.";
|
||||
if (clearScreenshotsFolderButton != null)
|
||||
clearScreenshotsFolderButton.tooltip = "Clear override and use the built-in default (Assets/Screenshots).";
|
||||
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";
|
||||
|
||||
if (autoStartOnLoadToggle != null)
|
||||
{
|
||||
autoStartOnLoadToggle.tooltip = "Automatically start the local HTTP server and connect the MCP bridge when the Unity Editor opens. Only applies to HTTP transport (stdio always auto-starts).";
|
||||
var autoStartLabel = autoStartOnLoadToggle.parent?.Q<Label>();
|
||||
if (autoStartLabel != null)
|
||||
autoStartLabel.tooltip = autoStartOnLoadToggle.tooltip;
|
||||
autoStartOnLoadToggle.SetValueWithoutNotify(EditorPrefs.GetBool(EditorPrefKeys.AutoStartOnLoad, false));
|
||||
}
|
||||
|
||||
gitUrlOverride.value = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");
|
||||
|
||||
bool debugEnabled = EditorPrefs.GetBool(EditorPrefKeys.DebugLogs, false);
|
||||
debugLogsToggle.value = debugEnabled;
|
||||
McpLog.SetDebugLoggingEnabled(debugEnabled);
|
||||
|
||||
if (logRecordToggle != null)
|
||||
logRecordToggle.value = McpLogRecord.IsEnabled;
|
||||
|
||||
devModeForceRefreshToggle.value = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
|
||||
if (allowLanHttpBindToggle != null)
|
||||
{
|
||||
allowLanHttpBindToggle.SetValueWithoutNotify(EditorPrefs.GetBool(EditorPrefKeys.AllowLanHttpBind, false));
|
||||
}
|
||||
if (allowInsecureRemoteHttpToggle != null)
|
||||
{
|
||||
allowInsecureRemoteHttpToggle.SetValueWithoutNotify(EditorPrefs.GetBool(EditorPrefKeys.AllowInsecureRemoteHttp, false));
|
||||
}
|
||||
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
|
||||
{
|
||||
url = ResolveServerPath(url);
|
||||
// Update the text field if the path was auto-corrected, without re-triggering the callback
|
||||
if (url != evt.newValue?.Trim())
|
||||
{
|
||||
gitUrlOverride.SetValueWithoutNotify(url);
|
||||
}
|
||||
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);
|
||||
});
|
||||
|
||||
if (logRecordToggle != null)
|
||||
{
|
||||
logRecordToggle.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
McpLogRecord.IsEnabled = evt.newValue;
|
||||
});
|
||||
}
|
||||
|
||||
if (autoStartOnLoadToggle != null)
|
||||
{
|
||||
autoStartOnLoadToggle.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
EditorPrefs.SetBool(EditorPrefKeys.AutoStartOnLoad, evt.newValue);
|
||||
});
|
||||
}
|
||||
|
||||
devModeForceRefreshToggle.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
EditorPrefs.SetBool(EditorPrefKeys.DevModeForceServerRefresh, evt.newValue);
|
||||
OnHttpServerCommandUpdateRequested?.Invoke();
|
||||
});
|
||||
|
||||
if (allowLanHttpBindToggle != null)
|
||||
{
|
||||
allowLanHttpBindToggle.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
EditorPrefs.SetBool(EditorPrefKeys.AllowLanHttpBind, evt.newValue);
|
||||
OnHttpServerCommandUpdateRequested?.Invoke();
|
||||
});
|
||||
}
|
||||
|
||||
if (allowInsecureRemoteHttpToggle != null)
|
||||
{
|
||||
allowInsecureRemoteHttpToggle.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
EditorPrefs.SetBool(EditorPrefKeys.AllowInsecureRemoteHttp, evt.newValue);
|
||||
OnHttpServerCommandUpdateRequested?.Invoke();
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
if (screenshotsFolderOverride != null)
|
||||
{
|
||||
screenshotsFolderOverride.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
ScreenshotPreferences.DefaultFolder = evt.newValue;
|
||||
});
|
||||
}
|
||||
if (browseScreenshotsFolderButton != null)
|
||||
{
|
||||
browseScreenshotsFolderButton.clicked += OnBrowseScreenshotsFolderClicked;
|
||||
}
|
||||
if (clearScreenshotsFolderButton != null)
|
||||
{
|
||||
clearScreenshotsFolderButton.clicked += () =>
|
||||
{
|
||||
ScreenshotPreferences.DefaultFolder = string.Empty;
|
||||
screenshotsFolderOverride?.SetValueWithoutNotify(string.Empty);
|
||||
};
|
||||
}
|
||||
|
||||
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, "");
|
||||
if (autoStartOnLoadToggle != null)
|
||||
autoStartOnLoadToggle.value = EditorPrefs.GetBool(EditorPrefKeys.AutoStartOnLoad, false);
|
||||
debugLogsToggle.value = EditorPrefs.GetBool(EditorPrefKeys.DebugLogs, false);
|
||||
if (logRecordToggle != null)
|
||||
logRecordToggle.value = McpLogRecord.IsEnabled;
|
||||
devModeForceRefreshToggle.value = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
|
||||
if (allowLanHttpBindToggle != null)
|
||||
{
|
||||
allowLanHttpBindToggle.value = EditorPrefs.GetBool(EditorPrefKeys.AllowLanHttpBind, false);
|
||||
}
|
||||
if (allowInsecureRemoteHttpToggle != null)
|
||||
{
|
||||
allowInsecureRemoteHttpToggle.value = EditorPrefs.GetBool(EditorPrefKeys.AllowInsecureRemoteHttp, false);
|
||||
}
|
||||
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 (containing pyproject.toml)", string.Empty, string.Empty);
|
||||
if (!string.IsNullOrEmpty(picked))
|
||||
{
|
||||
picked = ResolveServerPath(picked);
|
||||
gitUrlOverride.value = picked;
|
||||
EditorPrefs.SetString(EditorPrefKeys.GitUrlOverride, picked);
|
||||
OnGitUrlChanged?.Invoke();
|
||||
OnHttpServerCommandUpdateRequested?.Invoke();
|
||||
McpLog.Info($"Server source override set to: {picked}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates and auto-corrects a local server path to ensure it points to the directory
|
||||
/// containing pyproject.toml (the Python package root). If the user selects a parent
|
||||
/// directory (e.g. the repo root), this checks for a "Server" subdirectory with
|
||||
/// pyproject.toml and returns that instead.
|
||||
/// </summary>
|
||||
private static string ResolveServerPath(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return path;
|
||||
|
||||
// If path is not a local filesystem path, return as-is (git URLs, PyPI refs, etc.)
|
||||
if (path.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
|
||||
path.StartsWith("https://", StringComparison.OrdinalIgnoreCase) ||
|
||||
path.StartsWith("git+", StringComparison.OrdinalIgnoreCase) ||
|
||||
path.StartsWith("ssh://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
// Strip file:// prefix for filesystem checks, but preserve it for the return value
|
||||
string checkPath = path;
|
||||
string prefix = string.Empty;
|
||||
if (checkPath.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
prefix = "file://";
|
||||
checkPath = checkPath.Substring(7);
|
||||
}
|
||||
|
||||
// Already points to a directory with pyproject.toml — correct path
|
||||
if (File.Exists(Path.Combine(checkPath, "pyproject.toml")))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
// Check if "Server" subdirectory contains pyproject.toml (common repo structure)
|
||||
string serverSubDir = Path.Combine(checkPath, "Server");
|
||||
if (File.Exists(Path.Combine(serverSubDir, "pyproject.toml")))
|
||||
{
|
||||
string corrected = prefix + serverSubDir;
|
||||
McpLog.Info($"Auto-corrected server path to 'Server' subdirectory: {corrected}");
|
||||
return corrected;
|
||||
}
|
||||
|
||||
// Return as-is; uvx will report the error if the path is invalid
|
||||
return path;
|
||||
}
|
||||
|
||||
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 OnBrowseScreenshotsFolderClicked()
|
||||
{
|
||||
// Start the picker at the project's Assets/ since that's the most common target.
|
||||
string startDir = UnityEngine.Application.dataPath;
|
||||
string picked = EditorUtility.OpenFolderPanel("Select Screenshots Folder (inside this project)", startDir, string.Empty);
|
||||
if (string.IsNullOrEmpty(picked))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string projectRoot = Path.GetFullPath(Path.Combine(UnityEngine.Application.dataPath, "..")).Replace('\\', '/');
|
||||
string normalizedRoot = projectRoot.EndsWith("/") ? projectRoot : projectRoot + "/";
|
||||
string normalizedPicked = picked.Replace('\\', '/');
|
||||
|
||||
if (normalizedPicked.Equals(projectRoot, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Storing "" would wipe the EditorPrefs key (= "unset"), so reject the project
|
||||
// root rather than silently revert the override the user just chose.
|
||||
EditorUtility.DisplayDialog(
|
||||
"Pick a Subfolder",
|
||||
"Please pick a subfolder of the project (for example 'Assets/Screenshots' or 'Captures'). " +
|
||||
"Selecting the project root would mix screenshots in with your project files.",
|
||||
"OK");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!normalizedPicked.StartsWith(normalizedRoot, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
EditorUtility.DisplayDialog(
|
||||
"Folder Outside Project",
|
||||
$"The selected folder is outside the Unity project root.\n\nPicked: {normalizedPicked}\nProject: {projectRoot}\n\nPlease pick a folder inside the project.",
|
||||
"OK");
|
||||
return;
|
||||
}
|
||||
|
||||
string projectRelative = normalizedPicked.Substring(normalizedRoot.Length);
|
||||
|
||||
ScreenshotPreferences.DefaultFolder = projectRelative;
|
||||
screenshotsFolderOverride?.SetValueWithoutNotify(projectRelative);
|
||||
McpLog.Info($"Default screenshots folder set to '{projectRelative}'.");
|
||||
}
|
||||
|
||||
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");
|
||||
OnPackageDeployed?.Invoke();
|
||||
}
|
||||
|
||||
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");
|
||||
OnPackageDeployed?.Invoke();
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bf87d9c1c3b287e4180379f65af95dca
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,90 @@
|
||||
<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="Log Record (Assets/mcp.log):" class="setting-label" />
|
||||
<ui:Toggle name="log-record-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="Auto-Start Server on Editor Load:" class="setting-label" />
|
||||
<ui:Toggle name="auto-start-on-load-toggle" class="setting-toggle" />
|
||||
</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="Allow LAN Bind (HTTP Local):" class="setting-label" />
|
||||
<ui:Toggle name="allow-lan-http-bind-toggle" class="setting-toggle" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement class="setting-row">
|
||||
<ui:Label text="Allow Insecure Remote HTTP:" class="setting-label" />
|
||||
<ui:Toggle name="allow-insecure-remote-http-toggle" class="setting-toggle" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement class="override-row" style="margin-top: 8px;">
|
||||
<ui:Label text="Screenshots Folder:" class="override-label" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement class="path-override-controls">
|
||||
<ui:TextField name="screenshots-folder-override" placeholder-text="Assets/Screenshots (default)" class="override-field" />
|
||||
<ui:Button name="browse-screenshots-folder-button" text="Select" class="icon-button" />
|
||||
<ui:Button name="clear-screenshots-folder-button" text="Clear" class="icon-button" />
|
||||
</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>
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d7e63a0b220a4c9458289415ad91e7df
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4f880cc3dc8a412982f5ee198048d002
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,376 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Security;
|
||||
using MCPForUnity.Editor.Services.AssetGen.Import;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace MCPForUnity.Editor.Windows.Components.AssetGen
|
||||
{
|
||||
/// <summary>
|
||||
/// Controller for the AI Asset Generation settings tab. This tab is CONFIG ONLY:
|
||||
/// it lets users enter/clear per-provider API keys, toggle providers on/off,
|
||||
/// presence-check a key, and set non-secret generation preferences.
|
||||
/// Generation itself is never triggered here — only via MCP tools / CLI.
|
||||
///
|
||||
/// Keys are written to the OS secure store (<see cref="SecureKeyStore"/>), never to
|
||||
/// EditorPrefs or the project. The stored key is never read back into the field; only
|
||||
/// its presence is surfaced through the status label.
|
||||
/// </summary>
|
||||
public class McpAssetGenSection
|
||||
{
|
||||
// Fixed provider lists. Each Id is both the SecureKeyStore key and the
|
||||
// AssetGenPrefs enable-flag id. All model/marketplace providers below emit GLB.
|
||||
private static readonly (string Id, string Label)[] ModelProviders =
|
||||
{
|
||||
("tripo", "Tripo"),
|
||||
("meshy", "Meshy"),
|
||||
("sketchfab", "Sketchfab"),
|
||||
};
|
||||
|
||||
private static readonly (string Id, string Label)[] ImageProviders =
|
||||
{
|
||||
("fal", "fal"),
|
||||
("openrouter", "OpenRouter"),
|
||||
};
|
||||
|
||||
// UI Elements
|
||||
private VisualElement providersContainer;
|
||||
private VisualElement gltfastNotice;
|
||||
private DropdownField formatDropdown;
|
||||
private TextField outputRootField;
|
||||
private Toggle autoNormalizeToggle;
|
||||
|
||||
// Per-provider enable toggles for the GLB-capable (model) providers, used to
|
||||
// recompute the glTFast notice when a toggle changes.
|
||||
private readonly List<(string Id, Toggle Toggle)> modelEnableToggles = new();
|
||||
|
||||
public VisualElement Root { get; private set; }
|
||||
|
||||
public McpAssetGenSection(VisualElement root)
|
||||
{
|
||||
Root = root;
|
||||
CacheUIElements();
|
||||
InitializeUI();
|
||||
RegisterCallbacks();
|
||||
}
|
||||
|
||||
private void CacheUIElements()
|
||||
{
|
||||
providersContainer = Root.Q<VisualElement>("assetgen-providers-container");
|
||||
gltfastNotice = Root.Q<VisualElement>("gltfast-notice");
|
||||
formatDropdown = Root.Q<DropdownField>("assetgen-format-dropdown");
|
||||
outputRootField = Root.Q<TextField>("assetgen-output-root");
|
||||
autoNormalizeToggle = Root.Q<Toggle>("assetgen-auto-normalize");
|
||||
}
|
||||
|
||||
private void InitializeUI()
|
||||
{
|
||||
// One-time choices + tooltips; the field values are populated by SyncFromPrefs.
|
||||
if (formatDropdown != null)
|
||||
{
|
||||
formatDropdown.choices = new List<string> { "glb", "fbx", "obj" };
|
||||
formatDropdown.tooltip = "Default container format for generated 3D models.";
|
||||
}
|
||||
|
||||
if (outputRootField != null)
|
||||
{
|
||||
outputRootField.tooltip =
|
||||
$"Project-relative folder where generated assets are written. Empty = {AssetGenPrefs.DefaultOutputRoot}.";
|
||||
}
|
||||
|
||||
if (autoNormalizeToggle != null)
|
||||
{
|
||||
autoNormalizeToggle.tooltip = "Uniformly scale imported models to the target size on import.";
|
||||
}
|
||||
|
||||
SyncFromPrefs();
|
||||
}
|
||||
|
||||
private void RegisterCallbacks()
|
||||
{
|
||||
if (formatDropdown != null)
|
||||
{
|
||||
formatDropdown.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
AssetGenPrefs.DefaultFormat = evt.newValue;
|
||||
});
|
||||
}
|
||||
|
||||
if (outputRootField != null)
|
||||
{
|
||||
outputRootField.RegisterCallback<FocusOutEvent>(_ =>
|
||||
{
|
||||
AssetGenPrefs.OutputRoot = outputRootField.text?.Trim();
|
||||
// Reflect the normalized/default value (empty -> default) without re-triggering.
|
||||
outputRootField.SetValueWithoutNotify(AssetGenPrefs.OutputRoot);
|
||||
});
|
||||
}
|
||||
|
||||
if (autoNormalizeToggle != null)
|
||||
{
|
||||
autoNormalizeToggle.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
AssetGenPrefs.AutoNormalize = evt.newValue;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-reads secure-store presence and prefs and rebuilds the rows. Called when the
|
||||
/// tab becomes visible so keys set elsewhere (e.g. via CLI) are reflected.
|
||||
/// </summary>
|
||||
public void Refresh() => SyncFromPrefs();
|
||||
|
||||
/// <summary>Rebuild the provider rows and reflect current prefs into the fields.</summary>
|
||||
private void SyncFromPrefs()
|
||||
{
|
||||
BuildProviderRows();
|
||||
formatDropdown?.SetValueWithoutNotify(NormalizeFormat(AssetGenPrefs.DefaultFormat));
|
||||
outputRootField?.SetValueWithoutNotify(AssetGenPrefs.OutputRoot);
|
||||
autoNormalizeToggle?.SetValueWithoutNotify(AssetGenPrefs.AutoNormalize);
|
||||
UpdateGltfastNotice();
|
||||
}
|
||||
|
||||
private void BuildProviderRows()
|
||||
{
|
||||
if (providersContainer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
providersContainer.Clear();
|
||||
modelEnableToggles.Clear();
|
||||
|
||||
AddGroupLabel("3D Model Providers");
|
||||
foreach (var provider in ModelProviders)
|
||||
{
|
||||
var toggle = AddProviderRow(provider.Id, provider.Label);
|
||||
modelEnableToggles.Add((provider.Id, toggle));
|
||||
}
|
||||
|
||||
AddGroupLabel("Image Providers");
|
||||
foreach (var provider in ImageProviders)
|
||||
{
|
||||
AddProviderRow(provider.Id, provider.Label);
|
||||
}
|
||||
|
||||
AddBlenderHandoffRow();
|
||||
}
|
||||
|
||||
private void AddGroupLabel(string text)
|
||||
{
|
||||
var label = new Label(text);
|
||||
label.AddToClassList("config-label");
|
||||
providersContainer.Add(label);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Informational handoff row (not a keyed provider): best-effort "is Blender installed"
|
||||
/// status + a pointer to the blender-to-unity workflow. BlenderMCP itself runs in the AI
|
||||
/// client and isn't detectable from Unity, so this only reports the local Blender app.
|
||||
/// </summary>
|
||||
private void AddBlenderHandoffRow()
|
||||
{
|
||||
AddGroupLabel("Blender → Unity Handoff");
|
||||
|
||||
var row = new VisualElement();
|
||||
row.style.marginBottom = 8;
|
||||
|
||||
bool blender = BlenderDetection.IsInstalled();
|
||||
var status = new Label(blender ? "Blender app detected ✓" : "Blender app not found on this machine");
|
||||
status.AddToClassList("help-text");
|
||||
status.style.color = blender ? new Color(0.4f, 0.8f, 0.4f) : new Color(0.7f, 0.7f, 0.7f);
|
||||
row.Add(status);
|
||||
|
||||
var help = new Label(
|
||||
"Pair Blender with the BlenderMCP server in your AI client, then run the blender-to-unity " +
|
||||
"skill to export the current model — it imports via the import_model_file tool. (BlenderMCP " +
|
||||
"is configured in your AI client and can't be detected here.)");
|
||||
help.AddToClassList("help-text");
|
||||
help.style.whiteSpace = WhiteSpace.Normal;
|
||||
row.Add(help);
|
||||
|
||||
providersContainer.Add(row);
|
||||
}
|
||||
|
||||
private Toggle AddProviderRow(string id, string displayName)
|
||||
{
|
||||
var row = new VisualElement();
|
||||
row.style.marginBottom = 8;
|
||||
row.style.paddingBottom = 8;
|
||||
row.style.borderBottomWidth = 1;
|
||||
row.style.borderBottomColor = new Color(0.3f, 0.3f, 0.3f, 0.3f);
|
||||
|
||||
// Header: bold provider name + enable toggle.
|
||||
var header = new VisualElement();
|
||||
header.style.flexDirection = FlexDirection.Row;
|
||||
header.style.alignItems = Align.Center;
|
||||
header.style.marginBottom = 2;
|
||||
|
||||
var nameLabel = new Label(displayName);
|
||||
nameLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
nameLabel.style.flexGrow = 1;
|
||||
header.Add(nameLabel);
|
||||
|
||||
var enableToggle = new Toggle("Enabled");
|
||||
enableToggle.SetValueWithoutNotify(AssetGenPrefs.IsProviderEnabled(id));
|
||||
enableToggle.tooltip = $"Enable the {displayName} provider for asset generation.";
|
||||
header.Add(enableToggle);
|
||||
|
||||
row.Add(header);
|
||||
|
||||
var statusLabel = new Label();
|
||||
statusLabel.AddToClassList("help-text");
|
||||
|
||||
// Masked key field + Save / Clear / Test buttons.
|
||||
var fieldRow = new VisualElement();
|
||||
fieldRow.style.flexDirection = FlexDirection.Row;
|
||||
fieldRow.style.alignItems = Align.Center;
|
||||
|
||||
var keyField = new TextField();
|
||||
keyField.isPasswordField = true;
|
||||
keyField.maskChar = '*';
|
||||
keyField.style.flexGrow = 1;
|
||||
keyField.style.flexShrink = 1;
|
||||
keyField.style.marginRight = 4;
|
||||
keyField.tooltip =
|
||||
$"Paste your {displayName} API key, then press Save (or click away). " +
|
||||
"The key is stored in your OS secure store and is never read back into this field.";
|
||||
fieldRow.Add(keyField);
|
||||
|
||||
var saveButton = new Button { text = "Save" };
|
||||
saveButton.AddToClassList("icon-button");
|
||||
fieldRow.Add(saveButton);
|
||||
|
||||
var clearButton = new Button { text = "Clear" };
|
||||
clearButton.AddToClassList("icon-button");
|
||||
fieldRow.Add(clearButton);
|
||||
|
||||
var testButton = new Button { text = "Test" };
|
||||
testButton.AddToClassList("icon-button");
|
||||
fieldRow.Add(testButton);
|
||||
|
||||
row.Add(fieldRow);
|
||||
row.Add(statusLabel);
|
||||
|
||||
// Persist the typed key, then clear the field so the secret is never displayed.
|
||||
void SaveKeyFromField()
|
||||
{
|
||||
string text = keyField.text?.Trim();
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
SecureKeyStore.Current.Set(id, text);
|
||||
keyField.SetValueWithoutNotify(string.Empty);
|
||||
SetStatus(statusLabel, "saved ✓", true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Warn($"Failed to store {id} key: {ex.Message}");
|
||||
SetStatus(statusLabel, "save failed", false);
|
||||
}
|
||||
}
|
||||
|
||||
keyField.RegisterCallback<FocusOutEvent>(_ => SaveKeyFromField());
|
||||
saveButton.clicked += SaveKeyFromField;
|
||||
|
||||
clearButton.clicked += () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
SecureKeyStore.Current.Delete(id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Warn($"Failed to delete {id} key: {ex.Message}");
|
||||
}
|
||||
|
||||
keyField.SetValueWithoutNotify(string.Empty);
|
||||
SetStatus(statusLabel, "not set", false);
|
||||
};
|
||||
|
||||
// v1 surfaces presence only. Live endpoint validation (an actual auth ping to the
|
||||
// provider) is a future enhancement and intentionally not performed here.
|
||||
testButton.clicked += () =>
|
||||
{
|
||||
bool present = HasKey(id);
|
||||
SetStatus(statusLabel, present ? "key present ✓" : "no key set", present);
|
||||
};
|
||||
|
||||
enableToggle.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
AssetGenPrefs.SetProviderEnabled(id, evt.newValue);
|
||||
UpdateGltfastNotice();
|
||||
});
|
||||
|
||||
// Initial status reflects secure-store presence (existence only; never the value).
|
||||
bool has = HasKey(id);
|
||||
SetStatus(statusLabel, has ? "saved ✓" : "not set", has);
|
||||
|
||||
providersContainer.Add(row);
|
||||
return enableToggle;
|
||||
}
|
||||
|
||||
private static void SetStatus(Label label, string text, bool ok)
|
||||
{
|
||||
if (label == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
label.text = text;
|
||||
label.style.color = ok
|
||||
? new Color(0.4f, 0.8f, 0.4f)
|
||||
: new Color(0.7f, 0.7f, 0.7f);
|
||||
}
|
||||
|
||||
private static bool HasKey(string id)
|
||||
{
|
||||
try { return SecureKeyStore.Current.Has(id); }
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
private static string NormalizeFormat(string format)
|
||||
{
|
||||
switch ((format ?? string.Empty).Trim().ToLowerInvariant())
|
||||
{
|
||||
case "glb":
|
||||
case "fbx":
|
||||
case "obj":
|
||||
return format.Trim().ToLowerInvariant();
|
||||
default:
|
||||
return AssetGenPrefs.DefaultFormatValue;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateGltfastNotice()
|
||||
{
|
||||
if (gltfastNotice == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool anyGlbProviderEnabled = false;
|
||||
foreach (var entry in modelEnableToggles)
|
||||
{
|
||||
bool enabled = entry.Toggle != null
|
||||
? entry.Toggle.value
|
||||
: AssetGenPrefs.IsProviderEnabled(entry.Id);
|
||||
if (enabled)
|
||||
{
|
||||
anyGlbProviderEnabled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool show = anyGlbProviderEnabled && !ModelImportPipeline.IsGltfastAvailable();
|
||||
gltfastNotice.EnableInClassList("visible", show);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bdb478d5413049048560d485405b8b02
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,28 @@
|
||||
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="True">
|
||||
<Style src="../Common.uss" />
|
||||
<ui:VisualElement name="assetgen-section" class="section">
|
||||
<ui:Label text="AI Asset Generation" class="section-title" />
|
||||
<ui:VisualElement class="section-content">
|
||||
<ui:Label name="assetgen-help" class="validation-description" text="Enter your own provider API keys. Generation is triggered via MCP tools / CLI, not here. Keys are stored in your OS secure store (Keychain / Credential Manager / libsecret), never in the project." />
|
||||
<ui:VisualElement name="gltfast-notice" class="warning-banner">
|
||||
<ui:Label class="warning-banner-text" text="GLB import needs the glTFast package — install it from the Dependencies tab." />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement name="assetgen-providers-container" style="margin-top: 8px;" />
|
||||
<ui:Label text="Preferences" class="config-label" />
|
||||
<ui:VisualElement class="setting-row">
|
||||
<ui:Label text="Default 3D Format:" class="setting-label" />
|
||||
<ui:DropdownField name="assetgen-format-dropdown" class="setting-dropdown-inline" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement class="override-row" style="margin-top: 8px;">
|
||||
<ui:Label text="Output Root:" class="override-label" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement class="path-override-controls">
|
||||
<ui:TextField name="assetgen-output-root" placeholder-text="Assets/Generated" class="override-field" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement class="setting-row">
|
||||
<ui:Label text="Auto-Normalize Imported Models:" class="setting-label" />
|
||||
<ui:Toggle name="assetgen-auto-normalize" class="setting-toggle" />
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
</ui:UXML>
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3df8c86fc5da43c2b99611ea9f151338
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d20507fd66884816930ea8f431fd6ce8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,163 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace MCPForUnity.Editor.Windows.Components.Branding
|
||||
{
|
||||
/// <summary>
|
||||
/// The MCP for Unity "Ocean split-cube" brand mark.
|
||||
///
|
||||
/// On Unity 2022.1+ it is drawn as resolution-independent vector geometry via UI Toolkit's
|
||||
/// Painter2D — crisp at any DPI, theme-independent (fixed brand colors matching the source
|
||||
/// SVG), and with no package dependency. Painter2D does not exist on the 2021.3 floor, so
|
||||
/// there we fall back to the raster brand icon (package-icon.png) that ships with the package.
|
||||
///
|
||||
/// Geometry is transcribed from website/static/img/logo-mark.svg (viewBox "30 30 140 140"),
|
||||
/// which remains the human-facing source of truth. Size the element via USS (width/height);
|
||||
/// the drawing scales to the resolved content box.
|
||||
/// </summary>
|
||||
public sealed class OceanMark : VisualElement
|
||||
{
|
||||
public OceanMark()
|
||||
{
|
||||
#if UNITY_2022_1_OR_NEWER
|
||||
generateVisualContent += OnGenerateVisualContent;
|
||||
RegisterCallback<GeometryChangedEvent>(_ => MarkDirtyRepaint());
|
||||
#else
|
||||
ApplyRasterFallback();
|
||||
#endif
|
||||
pickingMode = PickingMode.Ignore; // purely decorative
|
||||
}
|
||||
|
||||
#if UNITY_2022_1_OR_NEWER
|
||||
// The source SVG viewBox spans 30..170 on both axes (a 140-unit square).
|
||||
private const float SvgOrigin = 30f;
|
||||
private const float SvgSize = 140f;
|
||||
|
||||
// Brand palette (hex values lifted directly from logo-mark.svg).
|
||||
private static readonly Color LeftFill = FromHex(0x2563EB); // blue cube faces
|
||||
private static readonly Color LeftStroke = FromHex(0x60A5FA); // blue cube outline
|
||||
private static readonly Color RightFill = FromHex(0x0D9488); // teal cube faces
|
||||
private static readonly Color RightStroke = FromHex(0x2DD4BF);// teal cube outline
|
||||
private static readonly Color Bridge = FromHex(0x22D3EE); // cyan bridge
|
||||
|
||||
private void OnGenerateVisualContent(MeshGenerationContext mgc)
|
||||
{
|
||||
Rect rect = contentRect;
|
||||
float box = Mathf.Min(rect.width, rect.height);
|
||||
if (box <= 1f) return; // not laid out yet / too small to draw
|
||||
|
||||
float scale = box / SvgSize;
|
||||
Vector2 P(float x, float y) => MapSvgPoint(x, y, rect);
|
||||
float S(float w) => w * scale;
|
||||
|
||||
Painter2D p = mgc.painter2D;
|
||||
p.lineJoin = LineJoin.Round;
|
||||
p.lineCap = LineCap.Round;
|
||||
|
||||
// --- Left cube (blue) ---
|
||||
FillPoly(p, WithAlpha(LeftFill, 0.22f), P(94, 40), P(42, 70), P(94, 100));
|
||||
FillPoly(p, WithAlpha(LeftFill, 0.12f), P(42, 70), P(42, 130), P(94, 160), P(94, 100));
|
||||
StrokePath(p, LeftStroke, S(3f), true, P(94, 40), P(42, 70), P(42, 130), P(94, 160));
|
||||
StrokePath(p, WithAlpha(LeftStroke, 0.65f), S(1.8f), false, P(42, 70), P(94, 100));
|
||||
|
||||
// --- Right cube (teal) ---
|
||||
FillPoly(p, WithAlpha(RightFill, 0.22f), P(106, 40), P(158, 70), P(106, 100));
|
||||
FillPoly(p, WithAlpha(RightFill, 0.12f), P(158, 70), P(158, 130), P(106, 160), P(106, 100));
|
||||
StrokePath(p, RightStroke, S(3f), true, P(106, 40), P(158, 70), P(158, 130), P(106, 160));
|
||||
StrokePath(p, WithAlpha(RightStroke, 0.65f), S(1.8f), false, P(158, 70), P(106, 100));
|
||||
|
||||
// --- Bridge rungs (cyan) ---
|
||||
StrokePath(p, Bridge, S(3f), false, P(94, 70), P(106, 70));
|
||||
StrokePath(p, Bridge, S(3f), false, P(94, 100), P(106, 100));
|
||||
StrokePath(p, Bridge, S(3f), false, P(94, 130), P(106, 130));
|
||||
|
||||
// --- Bridge dots (cyan) ---
|
||||
float dot = S(2.2f);
|
||||
FillDot(p, Bridge, P(94, 70), dot);
|
||||
FillDot(p, Bridge, P(106, 70), dot);
|
||||
FillDot(p, Bridge, P(94, 100), dot);
|
||||
FillDot(p, Bridge, P(106, 100), dot);
|
||||
FillDot(p, Bridge, P(94, 130), dot);
|
||||
FillDot(p, Bridge, P(106, 130), dot);
|
||||
}
|
||||
|
||||
private static void FillPoly(Painter2D p, Color color, params Vector2[] pts)
|
||||
{
|
||||
p.fillColor = color;
|
||||
p.BeginPath();
|
||||
p.MoveTo(pts[0]);
|
||||
for (int i = 1; i < pts.Length; i++) p.LineTo(pts[i]);
|
||||
p.ClosePath();
|
||||
p.Fill();
|
||||
}
|
||||
|
||||
private static void StrokePath(Painter2D p, Color color, float width, bool closed, params Vector2[] pts)
|
||||
{
|
||||
p.strokeColor = color;
|
||||
p.lineWidth = width;
|
||||
p.BeginPath();
|
||||
p.MoveTo(pts[0]);
|
||||
for (int i = 1; i < pts.Length; i++) p.LineTo(pts[i]);
|
||||
if (closed) p.ClosePath();
|
||||
p.Stroke();
|
||||
}
|
||||
|
||||
private static void FillDot(Painter2D p, Color color, Vector2 center, float radius)
|
||||
{
|
||||
if (radius <= 0.01f) return;
|
||||
p.fillColor = color;
|
||||
p.BeginPath();
|
||||
p.Arc(center, radius, new Angle(0f, AngleUnit.Degree), new Angle(360f, AngleUnit.Degree));
|
||||
p.Fill();
|
||||
}
|
||||
|
||||
private static Color WithAlpha(Color c, float a)
|
||||
{
|
||||
c.a = a;
|
||||
return c;
|
||||
}
|
||||
|
||||
/// <summary>Map a point from SVG viewBox space into the element's content rect (square, centered).</summary>
|
||||
private static Vector2 MapSvgPoint(float svgX, float svgY, Rect content)
|
||||
{
|
||||
float box = Mathf.Min(content.width, content.height);
|
||||
float scale = box / SvgSize;
|
||||
float offsetX = (content.width - box) * 0.5f;
|
||||
float offsetY = (content.height - box) * 0.5f;
|
||||
return new Vector2(
|
||||
offsetX + (svgX - SvgOrigin) * scale,
|
||||
offsetY + (svgY - SvgOrigin) * scale);
|
||||
}
|
||||
|
||||
/// <summary>Convert a 0xRRGGBB literal into an opaque Color.</summary>
|
||||
private static Color FromHex(int rgb) => new Color(
|
||||
((rgb >> 16) & 0xFF) / 255f,
|
||||
((rgb >> 8) & 0xFF) / 255f,
|
||||
(rgb & 0xFF) / 255f,
|
||||
1f);
|
||||
#else
|
||||
// Painter2D is unavailable before Unity 2022.1 — show the raster brand icon instead.
|
||||
private void ApplyRasterFallback()
|
||||
{
|
||||
Texture2D tex = LoadBrandTexture();
|
||||
if (tex == null) return;
|
||||
style.backgroundImage = new StyleBackground(tex);
|
||||
style.unityBackgroundScaleMode = ScaleMode.ScaleToFit;
|
||||
}
|
||||
|
||||
private static Texture2D LoadBrandTexture()
|
||||
{
|
||||
try
|
||||
{
|
||||
string root = MCPForUnity.Editor.Helpers.AssetPathUtility.GetMcpPackageRootPath();
|
||||
return UnityEditor.AssetDatabase.LoadAssetAtPath<Texture2D>($"{root}/package-icon.png");
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
MCPForUnity.Editor.Helpers.McpLog.Warn($"OceanMark: failed to load brand icon fallback: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a5a6cc86fe9844309b97362a718c3edd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4d9f5ceeb24166f47804e094440b7846
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,864 @@
|
||||
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 MCPForUnity.Editor.Setup;
|
||||
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 Button installSkillsButton;
|
||||
private VisualElement claudeCliPathRow;
|
||||
private TextField claudeCliPath;
|
||||
private Button browseClaudeButton;
|
||||
private VisualElement clientProjectDirRow;
|
||||
private TextField clientProjectDirField;
|
||||
private Button browseProjectDirButton;
|
||||
private Button clearProjectDirButton;
|
||||
private Foldout clientDetailsFoldout;
|
||||
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;
|
||||
private bool isSkillSyncInProgress;
|
||||
|
||||
// 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;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when a config mismatch is detected (e.g., version mismatch).
|
||||
/// The parameter contains the client name and the mismatch message (null if no mismatch).
|
||||
/// </summary>
|
||||
public event Action<string, string> OnClientConfigMismatch;
|
||||
|
||||
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");
|
||||
installSkillsButton = Root.Q<Button>("install-skills-button");
|
||||
claudeCliPathRow = Root.Q<VisualElement>("claude-cli-path-row");
|
||||
claudeCliPath = Root.Q<TextField>("claude-cli-path");
|
||||
browseClaudeButton = Root.Q<Button>("browse-claude-button");
|
||||
clientProjectDirRow = Root.Q<VisualElement>("client-project-dir-row");
|
||||
clientProjectDirField = Root.Q<TextField>("client-project-dir");
|
||||
browseProjectDirButton = Root.Q<Button>("browse-project-dir-button");
|
||||
clearProjectDirButton = Root.Q<Button>("clear-project-dir-button");
|
||||
clientDetailsFoldout = Root.Q<Foldout>("client-details-foldout");
|
||||
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;
|
||||
}
|
||||
|
||||
// Default the per-client setup foldout to expanded. The Configure / Unregister
|
||||
// button for the currently-selected client lives inside it, and the 9.7.0
|
||||
// default-collapsed behavior made users believe the Unregister action had been
|
||||
// removed (community report: "the Unregister button was removed in 9.7.0,
|
||||
// something is stuck on stdio and I can't switch to local"). The state still
|
||||
// persists per-user, so anyone who explicitly collapses it keeps that preference.
|
||||
if (clientDetailsFoldout != null)
|
||||
{
|
||||
clientDetailsFoldout.value = EditorPrefs.GetBool(EditorPrefKeys.ClientDetailsFoldoutOpen, true);
|
||||
clientDetailsFoldout.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
EditorPrefs.SetBool(EditorPrefKeys.ClientDetailsFoldoutOpen, evt.newValue);
|
||||
});
|
||||
}
|
||||
|
||||
var clientNames = configurators.Select(c => c.DisplayName).ToList();
|
||||
clientDropdown.choices = clientNames;
|
||||
if (clientNames.Count > 0)
|
||||
{
|
||||
// Restore last selected client from EditorPrefs
|
||||
string lastClientId = EditorPrefs.GetString(EditorPrefKeys.LastSelectedClientId, string.Empty);
|
||||
int restoredIndex = FindConfiguratorIndex(lastClientId);
|
||||
if (restoredIndex < 0)
|
||||
restoredIndex = 0;
|
||||
|
||||
clientDropdown.index = restoredIndex;
|
||||
selectedClientIndex = restoredIndex;
|
||||
}
|
||||
|
||||
claudeCliPathRow.style.display = DisplayStyle.None;
|
||||
clientProjectDirRow.style.display = DisplayStyle.None;
|
||||
|
||||
// Initialize the configuration display for the first selected client
|
||||
UpdateClientStatus();
|
||||
UpdateManualConfiguration();
|
||||
UpdateClaudeCliPathVisibility();
|
||||
UpdateClientProjectDirVisibility();
|
||||
UpdateInstallSkillsVisibility();
|
||||
}
|
||||
|
||||
private void RegisterCallbacks()
|
||||
{
|
||||
clientDropdown.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
int selectedIndex = GetIndexForDropdownValue(evt.newValue);
|
||||
if (selectedIndex < 0)
|
||||
{
|
||||
selectedIndex = clientDropdown.index;
|
||||
}
|
||||
if (selectedIndex < 0 || selectedIndex >= configurators.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
selectedClientIndex = selectedIndex;
|
||||
// Persist the selected client so it's restored on next window open
|
||||
if (selectedClientIndex >= 0 && selectedClientIndex < configurators.Count)
|
||||
{
|
||||
EditorPrefs.SetString(EditorPrefKeys.LastSelectedClientId, configurators[selectedClientIndex].Id);
|
||||
}
|
||||
UpdateClientStatus();
|
||||
UpdateManualConfiguration();
|
||||
UpdateClaudeCliPathVisibility();
|
||||
UpdateClientProjectDirVisibility();
|
||||
UpdateInstallSkillsVisibility();
|
||||
});
|
||||
|
||||
configureAllButton.clicked += OnConfigureAllClientsClicked;
|
||||
configureButton.clicked += OnConfigureClicked;
|
||||
installSkillsButton.clicked += OnInstallSkillsClicked;
|
||||
browseClaudeButton.clicked += OnBrowseClaudeClicked;
|
||||
browseProjectDirButton.clicked += OnBrowseProjectDirClicked;
|
||||
clearProjectDirButton.clicked += OnClearProjectDirClicked;
|
||||
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",
|
||||
McpStatus.VersionMismatch => "Version Mismatch",
|
||||
_ => "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 UpdateClientProjectDirVisibility()
|
||||
{
|
||||
if (selectedClientIndex < 0 || selectedClientIndex >= configurators.Count)
|
||||
return;
|
||||
|
||||
var client = configurators[selectedClientIndex];
|
||||
|
||||
if (client is ClaudeCliMcpConfigurator)
|
||||
{
|
||||
clientProjectDirRow.style.display = DisplayStyle.Flex;
|
||||
string projectDir = ClaudeCliMcpConfigurator.GetClientProjectDir();
|
||||
if (ClaudeCliMcpConfigurator.HasClientProjectDirOverride)
|
||||
{
|
||||
clientProjectDirField.value = projectDir + " (override)";
|
||||
}
|
||||
else
|
||||
{
|
||||
clientProjectDirField.value = projectDir;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
clientProjectDirRow.style.display = DisplayStyle.None;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnConfigureAllClientsClicked()
|
||||
{
|
||||
try
|
||||
{
|
||||
var summary = MCPServiceLocator.Client.ConfigureAllDetectedClients();
|
||||
|
||||
string headline = summary.SkippedCount > 0
|
||||
? $"{summary.SuccessCount + summary.FailureCount} detected client(s) processed. ({summary.SkippedCount} not installed, skipped.)"
|
||||
: summary.GetSummaryMessage();
|
||||
string message = headline + "\n\n";
|
||||
foreach (var msg in summary.Messages)
|
||||
{
|
||||
message += msg + "\n";
|
||||
}
|
||||
|
||||
EditorUtility.DisplayDialog("Configure Detected Clients", message, "OK");
|
||||
|
||||
// The bulk path mutated state for every detected client. Clear the per-client
|
||||
// status cache so any subsequent dropdown switch (or the currently-selected
|
||||
// client refresh below) reads fresh status from disk instead of the pre-bulk
|
||||
// value — otherwise every other client in the dropdown looks like it still
|
||||
// has Missing/IncorrectPath status until 45 s elapses on the throttle.
|
||||
lastStatusChecks.Clear();
|
||||
|
||||
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
|
||||
{
|
||||
// Per-client toggle: Configure→register, Unregister→remove. The Configure path
|
||||
// on JsonFile / Codex configurators is always idempotent-write so the bulk
|
||||
// "Configure All" can call it unconditionally; the toggle lives here in the UI
|
||||
// handler so the manual button still does what its label says.
|
||||
if (client.Status == McpStatus.Configured)
|
||||
{
|
||||
client.Unregister();
|
||||
}
|
||||
else
|
||||
{
|
||||
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..." : "Configuring...");
|
||||
|
||||
// Capture ALL main-thread-only values before async task
|
||||
string projectDir = ClaudeCliMcpConfigurator.GetClientProjectDir();
|
||||
bool useHttpTransport = EditorConfigurationCache.Instance.UseHttpTransport;
|
||||
string claudePath = MCPServiceLocator.Paths.GetClaudeCliPath();
|
||||
string httpUrl = HttpEndpointUtility.GetMcpRpcUrl();
|
||||
var (uvxPath, _, packageName) = AssetPathUtility.GetUvxCommandParts();
|
||||
string fromArgs = AssetPathUtility.GetBetaServerFromArgs(quoteFromPath: true);
|
||||
string uvxDevFlags = AssetPathUtility.GetUvxDevFlags();
|
||||
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, fromArgs, packageName, uvxDevFlags,
|
||||
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 UpdateInstallSkillsVisibility()
|
||||
{
|
||||
if (installSkillsButton == null)
|
||||
return;
|
||||
|
||||
bool visible = selectedClientIndex >= 0
|
||||
&& selectedClientIndex < configurators.Count
|
||||
&& configurators[selectedClientIndex].SupportsSkills;
|
||||
|
||||
installSkillsButton.style.display = visible ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
}
|
||||
|
||||
private void OnInstallSkillsClicked()
|
||||
{
|
||||
if (isSkillSyncInProgress)
|
||||
return;
|
||||
|
||||
if (selectedClientIndex < 0 || selectedClientIndex >= configurators.Count)
|
||||
return;
|
||||
|
||||
var client = configurators[selectedClientIndex];
|
||||
if (!client.SupportsSkills)
|
||||
return;
|
||||
|
||||
string installPath = client.GetSkillInstallPath();
|
||||
if (string.IsNullOrEmpty(installPath))
|
||||
return;
|
||||
|
||||
string branch = AssetPathUtility.IsPreReleaseVersion() ? "beta" : "main";
|
||||
|
||||
isSkillSyncInProgress = true;
|
||||
installSkillsButton.SetEnabled(false);
|
||||
installSkillsButton.text = "Syncing...";
|
||||
|
||||
SkillSyncService.SyncAsync(installPath, branch, null, result =>
|
||||
{
|
||||
isSkillSyncInProgress = false;
|
||||
installSkillsButton.SetEnabled(true);
|
||||
installSkillsButton.text = "Install Skills";
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
bool noChanges = result.Added == 0 && result.Updated == 0 && result.Deleted == 0;
|
||||
string summary = noChanges
|
||||
? "Skills are already up to date."
|
||||
: $"Added: {result.Added}, Updated: {result.Updated}, Deleted: {result.Deleted}";
|
||||
McpLog.Info($"SkillSync complete: {summary} ({installPath})");
|
||||
EditorUtility.DisplayDialog("Install Skills",
|
||||
$"{summary}\n\nInstalled at: {installPath}", "OK");
|
||||
}
|
||||
else
|
||||
{
|
||||
McpLog.Error($"SkillSync failed: {result.Error}");
|
||||
EditorUtility.DisplayDialog("Install Skills Failed", result.Error, "OK");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 OnBrowseProjectDirClicked()
|
||||
{
|
||||
string currentDir = ClaudeCliMcpConfigurator.GetClientProjectDir();
|
||||
string picked = EditorUtility.OpenFolderPanel("Select Client Project Directory", currentDir, "");
|
||||
if (!string.IsNullOrEmpty(picked))
|
||||
{
|
||||
if (!Directory.Exists(picked))
|
||||
{
|
||||
EditorUtility.DisplayDialog("Invalid Path", "The selected directory does not exist.", "OK");
|
||||
return;
|
||||
}
|
||||
EditorPrefs.SetString(EditorPrefKeys.ClientProjectDirOverride, picked);
|
||||
UpdateClientProjectDirVisibility();
|
||||
UpdateClientStatus();
|
||||
McpLog.Info($"Client project directory override set to: {picked}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnClearProjectDirClicked()
|
||||
{
|
||||
EditorPrefs.DeleteKey(EditorPrefKeys.ClientProjectDirOverride);
|
||||
UpdateClientProjectDirVisibility();
|
||||
UpdateClientStatus();
|
||||
McpLog.Info("Client project directory override cleared, using Unity project directory.");
|
||||
}
|
||||
|
||||
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 = ClaudeCliMcpConfigurator.GetClientProjectDir();
|
||||
bool useHttpTransport = EditorConfigurationCache.Instance.UseHttpTransport;
|
||||
string claudePath = MCPServiceLocator.Paths.GetClaudeCliPath();
|
||||
RuntimePlatform platform = Application.platform;
|
||||
bool isRemoteScope = HttpEndpointUtility.IsRemoteScope();
|
||||
// Get expected package source based on installed package version and overrides.
|
||||
string expectedPackageSource = GetExpectedPackageSourceForCurrentPackage();
|
||||
bool hasProjectDirOverride = ClaudeCliMcpConfigurator.HasClientProjectDirOverride;
|
||||
|
||||
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, platform, isRemoteScope, expectedPackageSource, attemptAutoRewrite: false, hasProjectDirOverride: hasProjectDirOverride);
|
||||
}
|
||||
}).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).
|
||||
// Skip when a project dir override is active — the registered transport
|
||||
// in the overridden project may legitimately differ from the local server.
|
||||
bool hasTransportMismatch = false;
|
||||
if (client.ConfiguredTransport != ConfiguredTransport.Unknown
|
||||
&& !ClaudeCliMcpConfigurator.HasClientProjectDirOverride)
|
||||
{
|
||||
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:
|
||||
case McpStatus.Error:
|
||||
case McpStatus.VersionMismatch:
|
||||
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);
|
||||
|
||||
// Notify listeners about version mismatch if applicable
|
||||
if (client.Status == McpStatus.VersionMismatch && client is McpClientConfiguratorBase baseConfigurator)
|
||||
{
|
||||
// Get the mismatch reason from the configStatus field
|
||||
string mismatchReason = baseConfigurator.Client.configStatus;
|
||||
OnClientConfigMismatch?.Invoke(client.DisplayName, mismatchReason);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Clear any previous mismatch warning
|
||||
OnClientConfigMismatch?.Invoke(client.DisplayName, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the expected package source for validation based on installed package version.
|
||||
/// Uses the same logic as registration to ensure validation matches what was registered.
|
||||
/// MUST be called from the main thread due to EditorPrefs access.
|
||||
/// </summary>
|
||||
private static string GetExpectedPackageSourceForCurrentPackage()
|
||||
{
|
||||
return AssetPathUtility.GetMcpServerPackageSource();
|
||||
}
|
||||
|
||||
private int FindConfiguratorIndex(string persistedClientValue)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(persistedClientValue))
|
||||
return -1;
|
||||
|
||||
// Primary match: stored stable ID.
|
||||
for (int i = 0; i < configurators.Count; i++)
|
||||
{
|
||||
if (string.Equals(configurators[i].Id, persistedClientValue, StringComparison.OrdinalIgnoreCase))
|
||||
return i;
|
||||
}
|
||||
|
||||
// Compatibility match for older persisted values (e.g., display names with spaces).
|
||||
string normalized = NormalizeClientToken(persistedClientValue);
|
||||
if (string.IsNullOrEmpty(normalized))
|
||||
return -1;
|
||||
|
||||
for (int i = 0; i < configurators.Count; i++)
|
||||
{
|
||||
if (NormalizeClientToken(configurators[i].Id) == normalized ||
|
||||
NormalizeClientToken(configurators[i].DisplayName) == normalized)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private int GetIndexForDropdownValue(string dropdownValue)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dropdownValue))
|
||||
return -1;
|
||||
|
||||
int directIndex = clientDropdown.choices?.IndexOf(dropdownValue) ?? -1;
|
||||
if (directIndex >= 0 && directIndex < configurators.Count)
|
||||
return directIndex;
|
||||
|
||||
string normalized = NormalizeClientToken(dropdownValue);
|
||||
if (string.IsNullOrEmpty(normalized))
|
||||
return -1;
|
||||
|
||||
for (int i = 0; i < configurators.Count; i++)
|
||||
{
|
||||
if (NormalizeClientToken(configurators[i].DisplayName) == normalized)
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static string NormalizeClientToken(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return string.Empty;
|
||||
|
||||
return new string(value.Where(char.IsLetterOrDigit).ToArray()).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e29d88664440184e9c0165aadf02d46
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,51 @@
|
||||
<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:Button name="configure-all-button" text="Configure All Detected Clients" class="primary-button" />
|
||||
<ui:Foldout name="client-details-foldout" text="Per-client setup" value="true" class="client-details-foldout">
|
||||
<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:Button name="install-skills-button" text="Install Skills" class="action-button" style="display: none;" />
|
||||
</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:VisualElement class="setting-row" name="client-project-dir-row" style="display: none;">
|
||||
<ui:Label text="Client Project Dir:" class="setting-label-small" />
|
||||
<ui:TextField name="client-project-dir" readonly="true" class="path-display-field" />
|
||||
<ui:Button name="browse-project-dir-button" text="Browse" class="icon-button" />
|
||||
<ui:Button name="clear-project-dir-button" text="Clear" 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:Foldout>
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
</ui:UXML>
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: abdb7edaa375af049bd795c7a8b8a613
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
@@ -0,0 +1,706 @@
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* At narrow widths the connection status label (e.g. "Session Active (Project)") must wrap/shrink
|
||||
instead of sliding under the toggle button. Scoped to the connection section so other shared
|
||||
.status-text rows are unaffected; uses wrapping (not ellipsis) so the text stays fully readable. */
|
||||
#connection-section .status-container {
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#connection-section .status-text {
|
||||
flex-shrink: 1;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
#connection-section #connection-toggle {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
/* Primary one-click action — flagged as the recommended path. */
|
||||
.primary-button {
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
background-color: rgba(0, 175, 110, 1);
|
||||
color: rgba(255, 255, 255, 1);
|
||||
border-radius: 6px;
|
||||
border-width: 1px;
|
||||
border-color: rgba(0, 145, 90, 1);
|
||||
font-size: 13px;
|
||||
-unity-font-style: bold;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.primary-button:hover {
|
||||
background-color: rgba(0, 200, 130, 1);
|
||||
border-color: rgba(0, 170, 105, 1);
|
||||
}
|
||||
|
||||
.primary-button:active {
|
||||
background-color: rgba(0, 150, 90, 1);
|
||||
border-color: rgba(0, 125, 75, 1);
|
||||
}
|
||||
|
||||
.primary-button:disabled {
|
||||
background-color: rgba(70, 100, 85, 0.5);
|
||||
color: rgba(220, 220, 220, 0.6);
|
||||
border-color: rgba(70, 100, 85, 0.5);
|
||||
}
|
||||
|
||||
/* Foldout that hides the per-client (single-target) controls. */
|
||||
.client-details-foldout {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.client-details-foldout > .unity-foldout__toggle {
|
||||
-unity-font-style: bold;
|
||||
font-size: 11px;
|
||||
padding: 4px;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.client-details-foldout > .unity-foldout__content {
|
||||
margin-top: 6px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.unity-theme-light .client-details-foldout > .unity-foldout__toggle {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
/* 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: column;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tool-action-button {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
flex-basis: 0;
|
||||
min-width: 0;
|
||||
height: 26px;
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
-unity-text-overflow-position: end;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.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(200, 200, 200, 1);
|
||||
border-radius: 3px;
|
||||
border-width: 1px;
|
||||
border-color: rgba(160, 160, 160, 1);
|
||||
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;
|
||||
}
|
||||
|
||||
/* Group enable/disable checkbox in the foldout header */
|
||||
.group-header-checkbox {
|
||||
margin-left: 6px;
|
||||
margin-right: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.group-header-checkbox > .unity-toggle__input > .unity-toggle__checkmark {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 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(255, 255, 255, 0.9);
|
||||
background-color: rgba(90, 95, 105, 1);
|
||||
border-color: rgba(110, 115, 125, 1);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a3fae4a627f640749a80d5d1dc84ebe4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,57 @@
|
||||
<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 name="version-mismatch-warning" class="warning-banner">
|
||||
<ui:Label name="version-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>
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9943fa234c3a76a4198d2983bf96ab26
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 582ec97120b80401cb943b45d15425f9
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 465cdffd91f9d461caf4298ca322e3ab
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,17 @@
|
||||
<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:VisualElement style="flex-direction: row;">
|
||||
<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: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>
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b455fadaaad0a43c4bae9f3fe784c5c3
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c2f853b1b3974f829a2cc09d52d3d7ad
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,803 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
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 MCPForUnity.Editor.Services.Transport;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
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.
|
||||
/// Tools are grouped by their Group property (core first, then alphabetical).
|
||||
/// </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 Button reconfigureButton;
|
||||
private VisualElement categoryContainer;
|
||||
private List<ToolMetadata> allTools = new();
|
||||
private readonly Dictionary<string, Toggle> groupToggleMap = new();
|
||||
private readonly List<(Foldout foldout, string title, List<ToolMetadata> tools)> foldoutEntries = new();
|
||||
|
||||
/// <summary>Human-friendly names for tool groups shown in the UI.</summary>
|
||||
private static readonly Dictionary<string, string> GroupDisplayNames = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "core", "Core Tools" },
|
||||
{ "vfx", "VFX & Shaders" },
|
||||
{ "animation", "Animation" },
|
||||
{ "ui", "UI Toolkit" },
|
||||
{ "scripting_ext", "Scripting Extensions" },
|
||||
{ "testing", "Testing" },
|
||||
{ "probuilder", "ProBuilder — Experimental" },
|
||||
{ "profiling", "Profiling & Frame Debugger" },
|
||||
};
|
||||
|
||||
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");
|
||||
reconfigureButton = Root.Q<Button>("reconfigure-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 and stdio transports. 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();
|
||||
};
|
||||
}
|
||||
|
||||
if (reconfigureButton != null)
|
||||
{
|
||||
reconfigureButton.AddToClassList("tool-action-button");
|
||||
reconfigureButton.clicked += OnReconfigureClientsClicked;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds the tool list and synchronises toggle states.
|
||||
/// Tools are displayed in group-based foldouts: core first, then other
|
||||
/// groups alphabetically. Custom (non-built-in) tools appear in a
|
||||
/// separate "Custom Tools" foldout at the bottom.
|
||||
/// </summary>
|
||||
public void Refresh()
|
||||
{
|
||||
toolToggleMap.Clear();
|
||||
groupToggleMap.Clear();
|
||||
foldoutEntries.Clear();
|
||||
categoryContainer?.Clear();
|
||||
|
||||
var service = MCPServiceLocator.ToolDiscovery;
|
||||
allTools = service.DiscoverAllTools()
|
||||
.OrderBy(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)
|
||||
{
|
||||
bool isHttp = EditorConfigurationCache.Instance.UseHttpTransport;
|
||||
noteLabel.text = isHttp
|
||||
? "Changes apply after reconnecting or re-registering tools."
|
||||
: "Stdio mode: toggles sync at startup. After changing toggles, ask the AI to run manage_tools with action 'sync' to refresh.";
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasTools)
|
||||
{
|
||||
AddInfoLabel("No MCP tools found. Add classes decorated with [McpForUnityTool] to expose tools.");
|
||||
UpdateSummary();
|
||||
return;
|
||||
}
|
||||
|
||||
// Partition into built-in and custom
|
||||
var builtInTools = allTools.Where(IsBuiltIn).ToList();
|
||||
var customTools = allTools.Where(tool => !IsBuiltIn(tool)).ToList();
|
||||
|
||||
// Group built-in tools by their Group property
|
||||
var grouped = builtInTools
|
||||
.GroupBy(t => t.Group ?? "core")
|
||||
.ToDictionary(g => g.Key, g => g.OrderBy(t => t.Name, StringComparer.OrdinalIgnoreCase).ToList());
|
||||
|
||||
// Render "core" first, then remaining groups alphabetically
|
||||
if (grouped.TryGetValue("core", out var coreTools))
|
||||
{
|
||||
BuildCategory(GetGroupDisplayName("core"), "group-core", coreTools);
|
||||
grouped.Remove("core");
|
||||
}
|
||||
|
||||
foreach (var kvp in grouped.OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
BuildCategory(GetGroupDisplayName(kvp.Key), $"group-{kvp.Key}", kvp.Value);
|
||||
}
|
||||
|
||||
// Custom tools at the bottom
|
||||
if (customTools.Count > 0)
|
||||
{
|
||||
BuildCategory("Custom Tools", "custom", customTools);
|
||||
}
|
||||
|
||||
UpdateSummary();
|
||||
}
|
||||
|
||||
private static string GetGroupDisplayName(string group)
|
||||
{
|
||||
if (GroupDisplayNames.TryGetValue(group, out var displayName))
|
||||
return displayName;
|
||||
// Fallback: capitalize first letter
|
||||
return string.IsNullOrEmpty(group)
|
||||
? "Other"
|
||||
: char.ToUpper(group[0]) + group.Substring(1);
|
||||
}
|
||||
|
||||
private void BuildCategory(string title, string prefsSuffix, IEnumerable<ToolMetadata> tools)
|
||||
{
|
||||
var toolList = tools.ToList();
|
||||
if (toolList.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool isExperimental = string.Equals(prefsSuffix, "group-probuilder", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
int enabledCount = toolList.Count(t => MCPServiceLocator.ToolDiscovery.IsToolEnabled(t.Name));
|
||||
|
||||
// Default foldout state: core is open, others collapsed
|
||||
bool defaultOpen = prefsSuffix == "group-core";
|
||||
var foldout = new Foldout
|
||||
{
|
||||
text = $"{title} ({enabledCount}/{toolList.Count})",
|
||||
value = EditorPrefs.GetBool(EditorPrefKeys.ToolFoldoutStatePrefix + prefsSuffix, defaultOpen)
|
||||
};
|
||||
|
||||
foldout.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
if (evt.target != foldout) return;
|
||||
EditorPrefs.SetBool(EditorPrefKeys.ToolFoldoutStatePrefix + prefsSuffix, evt.newValue);
|
||||
});
|
||||
|
||||
// Add a checkbox into the foldout header to toggle all tools in this group
|
||||
bool allEnabled = enabledCount == toolList.Count;
|
||||
var groupCheckbox = new Toggle { value = allEnabled };
|
||||
groupCheckbox.AddToClassList("group-header-checkbox");
|
||||
groupCheckbox.tooltip = $"Toggle all tools in \"{title}\" on or off.";
|
||||
|
||||
// Prevent the click from propagating to the foldout expand/collapse toggle
|
||||
groupCheckbox.RegisterCallback<ClickEvent>(evt => evt.StopPropagation());
|
||||
groupCheckbox.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
evt.StopPropagation();
|
||||
SetGroupToolsState(toolList, evt.newValue, foldout, title);
|
||||
});
|
||||
|
||||
// Insert the checkbox into the foldout's own header toggle element
|
||||
foldout.Q<Toggle>()?.Add(groupCheckbox);
|
||||
groupToggleMap[prefsSuffix] = groupCheckbox;
|
||||
|
||||
foreach (var tool in toolList)
|
||||
{
|
||||
foldout.Add(CreateToolRow(tool));
|
||||
}
|
||||
|
||||
if (isExperimental)
|
||||
{
|
||||
var warning = new HelpBox(
|
||||
"ProBuilder support is experimental. Mesh editing operations may produce " +
|
||||
"unexpected results on complex topologies. Always save your scene before " +
|
||||
"performing destructive operations.",
|
||||
HelpBoxMessageType.Warning);
|
||||
warning.style.marginTop = 4;
|
||||
warning.style.marginBottom = 2;
|
||||
foldout.Insert(0, warning);
|
||||
}
|
||||
|
||||
foldoutEntries.Add((foldout, title, toolList));
|
||||
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);
|
||||
|
||||
// Skip auto-generated placeholder descriptions like "Tool: find_gameobjects"
|
||||
if (!string.IsNullOrWhiteSpace(tool.Description)
|
||||
&& !tool.Description.StartsWith("Tool: ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
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 (IsManageCameraTool(tool))
|
||||
{
|
||||
row.Add(CreateManageSceneActions());
|
||||
}
|
||||
|
||||
if (IsBatchExecuteTool(tool))
|
||||
{
|
||||
row.Add(CreateBatchExecuteSettings());
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
private void HandleToggleChange(
|
||||
ToolMetadata tool,
|
||||
bool enabled,
|
||||
bool updateSummary = true,
|
||||
bool reregisterTools = true)
|
||||
{
|
||||
MCPServiceLocator.ToolDiscovery.SetToolEnabled(tool.Name, enabled);
|
||||
|
||||
if (updateSummary)
|
||||
{
|
||||
UpdateSummary();
|
||||
UpdateFoldoutHeaders();
|
||||
SyncGroupToggles();
|
||||
}
|
||||
|
||||
if (reregisterTools)
|
||||
{
|
||||
// Trigger tool reregistration with connected MCP server
|
||||
ReregisterToolsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void ReregisterToolsAsync()
|
||||
{
|
||||
// Fire and forget - don't block UI thread
|
||||
var transportManager = MCPServiceLocator.TransportManager;
|
||||
var client = transportManager.GetClient(TransportMode.Http);
|
||||
if (client == null || !client.IsConnected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await client.ReregisterToolsAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Warn($"Failed to reregister tools: {ex}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void SetAllToolsState(bool enabled)
|
||||
{
|
||||
bool hasChanges = false;
|
||||
|
||||
foreach (var tool in allTools)
|
||||
{
|
||||
if (!toolToggleMap.TryGetValue(tool.Name, out var toggle))
|
||||
{
|
||||
bool currentEnabled = MCPServiceLocator.ToolDiscovery.IsToolEnabled(tool.Name);
|
||||
if (currentEnabled != enabled)
|
||||
{
|
||||
MCPServiceLocator.ToolDiscovery.SetToolEnabled(tool.Name, enabled);
|
||||
hasChanges = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (toggle.value == enabled)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
toggle.SetValueWithoutNotify(enabled);
|
||||
HandleToggleChange(tool, enabled, updateSummary: false, reregisterTools: false);
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
UpdateSummary();
|
||||
UpdateFoldoutHeaders();
|
||||
SyncGroupToggles();
|
||||
|
||||
if (hasChanges)
|
||||
{
|
||||
// Trigger a single reregistration after bulk change
|
||||
ReregisterToolsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetGroupToolsState(List<ToolMetadata> groupTools, bool enabled, Foldout foldout, string title)
|
||||
{
|
||||
bool hasChanges = false;
|
||||
|
||||
foreach (var tool in groupTools)
|
||||
{
|
||||
if (toolToggleMap.TryGetValue(tool.Name, out var toggle))
|
||||
{
|
||||
if (toggle.value != enabled)
|
||||
{
|
||||
toggle.SetValueWithoutNotify(enabled);
|
||||
HandleToggleChange(tool, enabled, updateSummary: false, reregisterTools: false);
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
bool currentEnabled = MCPServiceLocator.ToolDiscovery.IsToolEnabled(tool.Name);
|
||||
if (currentEnabled != enabled)
|
||||
{
|
||||
MCPServiceLocator.ToolDiscovery.SetToolEnabled(tool.Name, enabled);
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the foldout header count
|
||||
int enabledCount = groupTools.Count(t => MCPServiceLocator.ToolDiscovery.IsToolEnabled(t.Name));
|
||||
foldout.text = $"{title} ({enabledCount}/{groupTools.Count})";
|
||||
|
||||
// Sync global group toggles after group change
|
||||
SyncGroupToggles();
|
||||
|
||||
UpdateSummary();
|
||||
|
||||
if (hasChanges)
|
||||
{
|
||||
ReregisterToolsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Synchronises group toggle checkmarks with actual tool states.
|
||||
/// Called after individual tool toggles change so the group toggle
|
||||
/// stays accurate.
|
||||
/// </summary>
|
||||
private void SyncGroupToggles()
|
||||
{
|
||||
// We need the grouped tool lists to check states.
|
||||
var builtInTools = allTools.Where(IsBuiltIn).ToList();
|
||||
var grouped = builtInTools
|
||||
.GroupBy(t => t.Group ?? "core")
|
||||
.ToDictionary(g => g.Key, g => g.ToList());
|
||||
var customTools = allTools.Where(t => !IsBuiltIn(t)).ToList();
|
||||
|
||||
foreach (var kvp in groupToggleMap)
|
||||
{
|
||||
List<ToolMetadata> groupTools;
|
||||
if (kvp.Key == "custom")
|
||||
{
|
||||
groupTools = customTools;
|
||||
}
|
||||
else
|
||||
{
|
||||
string groupKey = kvp.Key.StartsWith("group-") ? kvp.Key.Substring(6) : kvp.Key;
|
||||
if (!grouped.TryGetValue(groupKey, out groupTools))
|
||||
continue;
|
||||
}
|
||||
|
||||
bool allEnabled = groupTools.All(t => MCPServiceLocator.ToolDiscovery.IsToolEnabled(t.Name));
|
||||
kvp.Value.SetValueWithoutNotify(allEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnReconfigureClientsClicked()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Re-register tools with the server (HTTP mode)
|
||||
ReregisterToolsAsync();
|
||||
|
||||
// Reconfigure all already-configured clients.
|
||||
// For CLI-based clients Configure() is a toggle (unregister if
|
||||
// configured, register if not), so we call it twice: first to
|
||||
// unregister, then to re-register with the updated tool set.
|
||||
var clients = MCPServiceLocator.Client.GetAllClients();
|
||||
int success = 0;
|
||||
int skipped = 0;
|
||||
var messages = new List<string>();
|
||||
|
||||
foreach (var client in clients)
|
||||
{
|
||||
try
|
||||
{
|
||||
client.CheckStatus(attemptAutoRewrite: false);
|
||||
|
||||
if (client.Status != McpStatus.Configured)
|
||||
{
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (client is ClaudeCliMcpConfigurator)
|
||||
{
|
||||
// Toggle off (unregister), then toggle on (register)
|
||||
MCPServiceLocator.Client.ConfigureClient(client);
|
||||
MCPServiceLocator.Client.ConfigureClient(client);
|
||||
}
|
||||
else
|
||||
{
|
||||
// JSON-file clients: rewrite is idempotent
|
||||
MCPServiceLocator.Client.ConfigureClient(client);
|
||||
}
|
||||
|
||||
success++;
|
||||
messages.Add($"✓ {client.DisplayName}: Reconfigured");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
messages.Add($"⚠ {client.DisplayName}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
string header = $"Reconfigured {success} client(s), skipped {skipped}.";
|
||||
string body = messages.Count > 0
|
||||
? header + "\n\n" + string.Join("\n", messages)
|
||||
: header;
|
||||
|
||||
EditorUtility.DisplayDialog("Reconfigure Clients", body, "OK");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
EditorUtility.DisplayDialog("Reconfigure Failed", ex.Message, "OK");
|
||||
McpLog.Error($"Reconfigure failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
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 UpdateFoldoutHeaders()
|
||||
{
|
||||
foreach (var (foldout, title, tools) in foldoutEntries)
|
||||
{
|
||||
int enabledCount = tools.Count(t => MCPServiceLocator.ToolDiscovery.IsToolEnabled(t.Name));
|
||||
foldout.text = $"{title} ({enabledCount}/{tools.Count})";
|
||||
}
|
||||
}
|
||||
|
||||
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 gameViewButton = new Button(OnManageSceneScreenshotClicked)
|
||||
{
|
||||
text = "Game View"
|
||||
};
|
||||
gameViewButton.AddToClassList("tool-action-button");
|
||||
gameViewButton.style.marginTop = 4;
|
||||
gameViewButton.tooltip = "Capture a game camera screenshot. Default: Assets/Screenshots (configurable in Advanced).";
|
||||
|
||||
var sceneViewButton = new Button(OnSceneViewScreenshotClicked)
|
||||
{
|
||||
text = "Scene View"
|
||||
};
|
||||
sceneViewButton.AddToClassList("tool-action-button");
|
||||
sceneViewButton.style.marginTop = 4;
|
||||
sceneViewButton.style.marginLeft = 4;
|
||||
sceneViewButton.tooltip = "Capture the active Scene View viewport. Default: Assets/Screenshots (configurable in Advanced).";
|
||||
|
||||
var multiviewButton = new Button(OnManageSceneMultiviewClicked)
|
||||
{
|
||||
text = "Multiview"
|
||||
};
|
||||
multiviewButton.AddToClassList("tool-action-button");
|
||||
multiviewButton.style.marginTop = 4;
|
||||
multiviewButton.style.marginLeft = 4;
|
||||
multiviewButton.tooltip = "Capture a 6-angle contact sheet around the scene centre. Default: Assets/Screenshots (configurable in Advanced).";
|
||||
|
||||
var captureLabel = new Label("Capture:");
|
||||
captureLabel.style.marginTop = 6;
|
||||
captureLabel.style.unityFontStyleAndWeight = UnityEngine.FontStyle.Normal;
|
||||
|
||||
var row = new VisualElement();
|
||||
row.style.flexDirection = FlexDirection.Row;
|
||||
row.style.flexWrap = Wrap.Wrap;
|
||||
row.Add(gameViewButton);
|
||||
row.Add(sceneViewButton);
|
||||
row.Add(multiviewButton);
|
||||
|
||||
actions.Add(captureLabel);
|
||||
actions.Add(row);
|
||||
return actions;
|
||||
}
|
||||
|
||||
private VisualElement CreateBatchExecuteSettings()
|
||||
{
|
||||
var container = new VisualElement();
|
||||
container.AddToClassList("tool-item-actions");
|
||||
container.style.flexDirection = FlexDirection.Row;
|
||||
container.style.alignItems = Align.Center;
|
||||
container.style.marginTop = 4;
|
||||
|
||||
var label = new Label("Max commands per batch:");
|
||||
label.style.marginRight = 8;
|
||||
label.style.unityFontStyleAndWeight = UnityEngine.FontStyle.Normal;
|
||||
container.Add(label);
|
||||
|
||||
int currentValue = EditorPrefs.GetInt(
|
||||
EditorPrefKeys.BatchExecuteMaxCommands,
|
||||
BatchExecute.DefaultMaxCommandsPerBatch
|
||||
);
|
||||
|
||||
var field = new IntegerField
|
||||
{
|
||||
value = Math.Clamp(currentValue, 1, BatchExecute.AbsoluteMaxCommandsPerBatch),
|
||||
style = { width = 60 }
|
||||
};
|
||||
field.tooltip = $"Number of commands allowed per batch_execute call (1–{BatchExecute.AbsoluteMaxCommandsPerBatch}). Default: {BatchExecute.DefaultMaxCommandsPerBatch}.";
|
||||
|
||||
field.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
int clamped = Math.Clamp(evt.newValue, 1, BatchExecute.AbsoluteMaxCommandsPerBatch);
|
||||
if (clamped != evt.newValue)
|
||||
{
|
||||
field.SetValueWithoutNotify(clamped);
|
||||
}
|
||||
EditorPrefs.SetInt(EditorPrefKeys.BatchExecuteMaxCommands, clamped);
|
||||
});
|
||||
|
||||
container.Add(field);
|
||||
|
||||
var hint = new Label($"(max {BatchExecute.AbsoluteMaxCommandsPerBatch})");
|
||||
hint.style.marginLeft = 4;
|
||||
hint.style.color = new UnityEngine.Color(0.5f, 0.5f, 0.5f);
|
||||
hint.style.fontSize = 10;
|
||||
container.Add(hint);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
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 void OnSceneViewScreenshotClicked()
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = ManageScene.ExecuteSceneViewScreenshot();
|
||||
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("Scene View screenshot capture requested.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Error($"Failed to capture Scene View screenshot: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnManageSceneMultiviewClicked()
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = ManageScene.ExecuteMultiviewScreenshot();
|
||||
if (response is SuccessResponse success)
|
||||
{
|
||||
// The data object is an anonymous type with imageBase64 — serialize to extract it
|
||||
var json = Newtonsoft.Json.Linq.JObject.FromObject(success.Data);
|
||||
string base64 = json["imageBase64"]?.ToString();
|
||||
if (!string.IsNullOrEmpty(base64))
|
||||
{
|
||||
string folderSpec = MCPForUnity.Editor.Helpers.ScreenshotPreferences.Resolve(null);
|
||||
string folder;
|
||||
try
|
||||
{
|
||||
folder = MCPForUnity.Runtime.Helpers.ScreenshotUtility.ResolveFolderAbsolute(folderSpec);
|
||||
}
|
||||
catch (System.InvalidOperationException ex)
|
||||
{
|
||||
McpLog.Error(ex.Message);
|
||||
return;
|
||||
}
|
||||
if (!System.IO.Directory.Exists(folder))
|
||||
System.IO.Directory.CreateDirectory(folder);
|
||||
|
||||
string fileName = $"Multiview_{System.DateTime.Now:yyyyMMdd_HHmmss}.png";
|
||||
string filePath = System.IO.Path.Combine(folder, fileName);
|
||||
System.IO.File.WriteAllBytes(filePath, Convert.FromBase64String(base64));
|
||||
|
||||
// Import only when the file landed under Assets/.
|
||||
string projectRoot = System.IO.Path.GetFullPath(System.IO.Path.Combine(UnityEngine.Application.dataPath, "..")).Replace('\\', '/');
|
||||
string normalizedFile = filePath.Replace('\\', '/');
|
||||
string normalizedRoot = projectRoot.EndsWith("/") ? projectRoot : projectRoot + "/";
|
||||
string projectRelative = normalizedFile.StartsWith(normalizedRoot, System.StringComparison.OrdinalIgnoreCase)
|
||||
? normalizedFile.Substring(normalizedRoot.Length)
|
||||
: normalizedFile;
|
||||
if (MCPForUnity.Runtime.Helpers.ScreenshotUtility.IsUnderAssets(projectRelative))
|
||||
AssetDatabase.ImportAsset(projectRelative, ImportAssetOptions.ForceSynchronousImport);
|
||||
|
||||
McpLog.Info($"Multiview contact sheet saved to {projectRelative}");
|
||||
}
|
||||
else
|
||||
{
|
||||
McpLog.Info(success.Message ?? "Multiview capture completed.");
|
||||
}
|
||||
}
|
||||
else if (response is ErrorResponse error && !string.IsNullOrWhiteSpace(error.Error))
|
||||
{
|
||||
McpLog.Error(error.Error);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Error($"Failed to capture multiview: {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 IsManageCameraTool(ToolMetadata tool) => string.Equals(tool?.Name, "manage_camera", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsBatchExecuteTool(ToolMetadata tool) => string.Equals(tool?.Name, "batch_execute", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsBuiltIn(ToolMetadata tool) => tool?.IsBuiltIn ?? false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c6b3eaf7efb642e89b9b9548458f72d6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,24 @@
|
||||
<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:VisualElement style="flex-direction: row;">
|
||||
<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:VisualElement>
|
||||
<ui:VisualElement style="flex-direction: row;">
|
||||
<ui:Button name="rescan-button" text="Rescan" class="tool-action-button" />
|
||||
<ui:Button name="reconfigure-button" text="Reconfigure Clients" class="tool-action-button" />
|
||||
</ui:VisualElement>
|
||||
</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>
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a94c7afd72c4dcf9f8a611d85c9a1e4
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f68f3b0ff9e214244ad7e57b106d5c60
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c65b5fd2ed3efbf469bbc0a089f845e3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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>
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3f682815a83bb6841ac61f7f399d903c
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: acc0b0b106a5e4826ab3fdbda7916eaf
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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>
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 06c99d6b0c7fa4fd3842e4d3b2a7407f
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
@@ -0,0 +1,420 @@
|
||||
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.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.ProjectScopedToolsLocalHttp, EditorPrefType.Bool },
|
||||
{ EditorPrefKeys.AllowLanHttpBind, EditorPrefType.Bool },
|
||||
{ EditorPrefKeys.AllowInsecureRemoteHttp, 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.LastSelectedClientId, 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()
|
||||
{
|
||||
// Clear search filter on GUI recreation to avoid stale filtered results
|
||||
searchFilter = "";
|
||||
|
||||
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))
|
||||
{
|
||||
// Check if the key actually exists
|
||||
item.IsUnset = !EditorPrefs.HasKey(key);
|
||||
|
||||
// Use the known type
|
||||
switch (knownType)
|
||||
{
|
||||
case EditorPrefType.Bool:
|
||||
item.Type = EditorPrefType.Bool;
|
||||
item.Value = item.IsUnset ? "Unset. Default: False" : EditorPrefs.GetBool(key, false).ToString();
|
||||
break;
|
||||
case EditorPrefType.Int:
|
||||
item.Type = EditorPrefType.Int;
|
||||
item.Value = item.IsUnset ? "Unset. Default: 0" : EditorPrefs.GetInt(key, 0).ToString();
|
||||
break;
|
||||
case EditorPrefType.Float:
|
||||
item.Type = EditorPrefType.Float;
|
||||
item.Value = item.IsUnset ? "Unset. Default: 0" : EditorPrefs.GetFloat(key, 0f).ToString();
|
||||
break;
|
||||
case EditorPrefType.String:
|
||||
item.Type = EditorPrefType.String;
|
||||
item.Value = item.IsUnset ? "Unset. Default: (empty)" : 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");
|
||||
|
||||
// Style unset items
|
||||
if (item.IsUnset)
|
||||
{
|
||||
valueField.SetEnabled(false);
|
||||
valueField.style.opacity = 0.6f;
|
||||
saveButton.SetEnabled(false);
|
||||
}
|
||||
|
||||
// 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; }
|
||||
public bool IsUnset { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EditorPrefs value types
|
||||
/// </summary>
|
||||
public enum EditorPrefType
|
||||
{
|
||||
String,
|
||||
Int,
|
||||
Float,
|
||||
Bool
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ceb71bbae267c4765aff72e4cc845ecb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0cfa716884c1445d8a5e9581bbe2e9ce
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f0c6e1d3e4d5e6f7a8b9c0d1e2f3a4d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,156 @@
|
||||
/* 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-left {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-right: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.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);
|
||||
flex-shrink: 0;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* Update Notification */
|
||||
.update-notification {
|
||||
display: none;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px 12px;
|
||||
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);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.update-notification.visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.update-notification-text {
|
||||
font-size: 11px;
|
||||
color: rgba(100, 200, 100, 1);
|
||||
white-space: normal;
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
-unity-text-align: middle-center;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
flex-shrink: 1;
|
||||
min-height: 32px;
|
||||
font-size: 12px;
|
||||
border-width: 0px;
|
||||
background-color: transparent;
|
||||
margin: 0px 2px 0px 0px;
|
||||
padding: 0px 8px;
|
||||
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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,40 @@
|
||||
<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:VisualElement name="header-left" class="header-left">
|
||||
<!-- OceanMark logo is injected here at index 0 in CreateGUI -->
|
||||
<ui:Label text="MCP for Unity" name="title" class="header-title" />
|
||||
</ui:VisualElement>
|
||||
<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="assetgen-tab" text="Asset Gen" />
|
||||
<uie:ToolbarToggle name="deps-tab" text="Deps" />
|
||||
<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="deps-panel" class="panel-scroll hidden" style="flex-grow: 1;">
|
||||
<ui:VisualElement name="deps-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:ScrollView name="assetgen-panel" class="panel-scroll hidden" style="flex-grow: 1;">
|
||||
<ui:VisualElement name="assetgen-container" class="section-stack" />
|
||||
</ui:ScrollView>
|
||||
</ui:VisualElement>
|
||||
</ui:UXML>
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7f8a4e9c1d2b3e4f5a6b7c8d9e0f1a2b
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
@@ -0,0 +1,375 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using MCPForUnity.Editor.Clients;
|
||||
using MCPForUnity.Editor.Dependencies;
|
||||
using MCPForUnity.Editor.Dependencies.Models;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Services;
|
||||
using MCPForUnity.Editor.Windows.Components.Branding;
|
||||
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 installUvButton;
|
||||
private Button refreshButton;
|
||||
private Button doneButton;
|
||||
|
||||
// Tracks an in-flight uv install so completion is handled on the main thread.
|
||||
private Task<UvInstaller.UvInstallResult> _uvInstallTask;
|
||||
|
||||
// Step 2 (Configure Clients) UI elements
|
||||
private VisualElement stepDeps;
|
||||
private VisualElement stepClients;
|
||||
private VisualElement clientsList;
|
||||
private Button skipClientsButton;
|
||||
private Button configureSelectedButton;
|
||||
private readonly List<(IMcpClientConfigurator client, Toggle toggle)> clientToggles = new();
|
||||
|
||||
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);
|
||||
|
||||
// Embed the Ocean brand mark beside the title
|
||||
var setupHeader = rootVisualElement.Q<VisualElement>("setup-header");
|
||||
if (setupHeader != null && setupHeader.Q<OceanMark>() == null)
|
||||
{
|
||||
var logo = new OceanMark { name = "setup-logo" };
|
||||
logo.AddToClassList("setup-logo");
|
||||
setupHeader.Insert(0, logo);
|
||||
}
|
||||
|
||||
// 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");
|
||||
installUvButton = rootVisualElement.Q<Button>("install-uv-button");
|
||||
refreshButton = rootVisualElement.Q<Button>("refresh-button");
|
||||
doneButton = rootVisualElement.Q<Button>("done-button");
|
||||
stepDeps = rootVisualElement.Q<VisualElement>("step-deps");
|
||||
stepClients = rootVisualElement.Q<VisualElement>("step-clients");
|
||||
clientsList = rootVisualElement.Q<VisualElement>("clients-list");
|
||||
skipClientsButton = rootVisualElement.Q<Button>("skip-clients-button");
|
||||
configureSelectedButton = rootVisualElement.Q<Button>("configure-selected-button");
|
||||
|
||||
// Register callbacks
|
||||
refreshButton.clicked += OnRefreshClicked;
|
||||
doneButton.clicked += OnDoneClicked;
|
||||
openPythonLinkButton.clicked += OnOpenPythonInstallClicked;
|
||||
openUvLinkButton.clicked += OnOpenUvInstallClicked;
|
||||
if (installUvButton != null) installUvButton.clicked += OnInstallUvClicked;
|
||||
skipClientsButton.clicked += OnSkipClientsClicked;
|
||||
configureSelectedButton.clicked += OnConfigureSelectedClicked;
|
||||
|
||||
// Initial update
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (_dependencyResult == null)
|
||||
{
|
||||
_dependencyResult = DependencyManager.CheckAllDependencies();
|
||||
}
|
||||
// Resume polling if a uv install was still in flight when the window was last disabled,
|
||||
// so its completion is still processed (button reset, dependencies re-checked).
|
||||
if (_uvInstallTask != null)
|
||||
{
|
||||
EditorApplication.update -= PollUvInstall;
|
||||
EditorApplication.update += PollUvInstall;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRefreshClicked()
|
||||
{
|
||||
_dependencyResult = DependencyManager.CheckAllDependencies();
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
private void OnDoneClicked()
|
||||
{
|
||||
if (_dependencyResult != null && _dependencyResult.IsSystemReady)
|
||||
{
|
||||
ShowClientsStep();
|
||||
}
|
||||
else
|
||||
{
|
||||
Setup.SetupWindowService.MarkSetupDismissed();
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowClientsStep()
|
||||
{
|
||||
stepDeps.style.display = DisplayStyle.None;
|
||||
stepClients.style.display = DisplayStyle.Flex;
|
||||
PopulateClientsList();
|
||||
}
|
||||
|
||||
private void PopulateClientsList()
|
||||
{
|
||||
clientsList.Clear();
|
||||
clientToggles.Clear();
|
||||
foreach (var c in McpClientRegistry.All)
|
||||
{
|
||||
if (!c.IsInstalled) continue;
|
||||
var toggle = new Toggle(c.DisplayName)
|
||||
{
|
||||
value = true,
|
||||
tooltip = c.GetConfigPath()
|
||||
};
|
||||
clientToggles.Add((c, toggle));
|
||||
clientsList.Add(toggle);
|
||||
}
|
||||
if (clientToggles.Count == 0)
|
||||
{
|
||||
clientsList.Add(new Label("No supported MCP clients detected on this machine. You can configure clients later from Tools → MCP for Unity."));
|
||||
configureSelectedButton.SetEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSkipClientsClicked()
|
||||
{
|
||||
Setup.SetupWindowService.MarkSetupCompleted();
|
||||
Close();
|
||||
}
|
||||
|
||||
private void OnConfigureSelectedClicked()
|
||||
{
|
||||
int success = 0, failure = 0;
|
||||
var failures = new List<string>();
|
||||
foreach (var (c, toggle) in clientToggles)
|
||||
{
|
||||
if (!toggle.value) continue;
|
||||
try
|
||||
{
|
||||
MCPServiceLocator.Client.ConfigureClient(c);
|
||||
success++;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
failure++;
|
||||
failures.Add($"⚠ {c.DisplayName}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
if (success == 0 && failure == 0)
|
||||
{
|
||||
EditorUtility.DisplayDialog(
|
||||
"Client Configuration",
|
||||
"No clients were selected. Tick at least one client to continue, or close the window to skip setup.",
|
||||
"OK");
|
||||
return;
|
||||
}
|
||||
// Keep the summary short: a count, only the failures (if any), and the next step —
|
||||
// no need to enumerate every successfully-configured client.
|
||||
string failureList = failures.Count > 0 ? "\n\n" + string.Join("\n", failures) : "";
|
||||
string nextStep = (failure == 0 && success > 0)
|
||||
? "\n\nYou're all set. Ask your AI assistant to create a GameObject in the open scene to confirm the connection."
|
||||
: "";
|
||||
EditorUtility.DisplayDialog(
|
||||
"Client Configuration",
|
||||
$"{success} configured, {failure} failed.{failureList}{nextStep}",
|
||||
"OK");
|
||||
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 OnInstallUvClicked()
|
||||
{
|
||||
if (_uvInstallTask != null) return; // already running
|
||||
|
||||
bool proceed = EditorUtility.DisplayDialog(
|
||||
"Install UV",
|
||||
"This will download and run the official uv installer:\n\n" +
|
||||
UvInstaller.DescribeCommand() +
|
||||
"\n\nContinue?",
|
||||
"Install",
|
||||
"Cancel");
|
||||
if (!proceed) return;
|
||||
|
||||
installUvButton.SetEnabled(false);
|
||||
installUvButton.text = "Installing UV…";
|
||||
statusMessage.text = "Installing uv… this can take a moment.";
|
||||
statusMessage.style.color = new StyleColor(new Color(1f, 0.6f, 0f));
|
||||
|
||||
_uvInstallTask = Task.Run(() => UvInstaller.Run());
|
||||
EditorApplication.update += PollUvInstall;
|
||||
}
|
||||
|
||||
private void PollUvInstall()
|
||||
{
|
||||
// The window/UI may have been torn down while the task ran — stop polling and drop it
|
||||
// (guards against dereferencing UI fields after teardown).
|
||||
if (installUvButton == null || installUvButton.panel == null)
|
||||
{
|
||||
EditorApplication.update -= PollUvInstall;
|
||||
return;
|
||||
}
|
||||
if (_uvInstallTask == null || !_uvInstallTask.IsCompleted) return;
|
||||
|
||||
EditorApplication.update -= PollUvInstall;
|
||||
var task = _uvInstallTask;
|
||||
_uvInstallTask = null;
|
||||
|
||||
installUvButton.SetEnabled(true);
|
||||
installUvButton.text = "Install UV Automatically";
|
||||
|
||||
// UvInstaller.Run catches its own exceptions, so the task always completes with a result.
|
||||
UvInstaller.UvInstallResult result = task.Result;
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
_dependencyResult = DependencyManager.CheckAllDependencies();
|
||||
UpdateUI();
|
||||
if (!_dependencyResult.IsSystemReady)
|
||||
{
|
||||
EditorUtility.DisplayDialog(
|
||||
"Install UV",
|
||||
"uv installed, but it isn't visible on PATH yet. Restart Unity (or your terminal) so it picks up the new PATH, then click Refresh.\n\n" +
|
||||
result.Output,
|
||||
"OK");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Reset the status label off the "Installing…" state before reporting the failure.
|
||||
UpdateUI();
|
||||
EditorUtility.DisplayDialog(
|
||||
"Install UV Failed",
|
||||
"The installer did not complete successfully. You can install uv manually via \"Open UV Install Page\".\n\n" +
|
||||
result.Output,
|
||||
"OK");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
EditorApplication.update -= PollUvInstall;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Offer the one-click uv installer only when uv is actually missing
|
||||
bool uvMissing = uvDep != null && !uvDep.IsAvailable;
|
||||
if (installUvButton != null)
|
||||
{
|
||||
bool showInstall = uvMissing && UvInstaller.IsSupported && _uvInstallTask == null;
|
||||
installUvButton.style.display = showInstall ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 62a298f9ec603ba489eaab14b97f1cea
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,143 @@
|
||||
/* MCP Setup Window Styles */
|
||||
|
||||
/* Root container */
|
||||
#root-container {
|
||||
padding: 20px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/* Header row (logo + title) */
|
||||
.setup-header {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.setup-logo {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
margin-right: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* In the header row the title reads as a plain heading beside the logo, not a boxed button. */
|
||||
.setup-header .title {
|
||||
margin-bottom: 0;
|
||||
padding: 0;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
flex-shrink: 1;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
/* 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 — wraps rather than clipping when the window is narrowed. */
|
||||
.button-container {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Button sizing — grow to fit the label (e.g. "Configure Selected") instead of clipping. */
|
||||
.setup-button {
|
||||
min-width: 96px;
|
||||
height: 28px;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
margin-left: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Footer buttons size to content and sit together on one row. The descendant selector
|
||||
(higher specificity) beats .secondary-button's width:100% regardless of stylesheet order,
|
||||
so Refresh no longer stretches full-width and overflows. */
|
||||
.button-container .setup-button {
|
||||
width: auto;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
/* Description text spacing */
|
||||
.description-text {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,78 @@
|
||||
<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:VisualElement name="setup-header" class="setup-header">
|
||||
<!-- OceanMark logo is injected here at index 0 in CreateGUI -->
|
||||
<ui:Label text="MCP for Unity Setup" name="title" class="title" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement name="step-deps">
|
||||
<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="install-uv-button" text="Install UV Automatically" class="action-button install-link-button" />
|
||||
<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="Next" class="setup-button action-button" />
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement name="step-clients" style="display: none;">
|
||||
<ui:VisualElement class="section">
|
||||
<ui:Label text="Configure MCP Clients" class="section-title" />
|
||||
<ui:Label text="We found the following MCP clients on your machine. Select which to configure:" class="help-text description-text" />
|
||||
<ui:ScrollView name="clients-scroll" mode="Vertical" style="max-height: 240px;">
|
||||
<ui:VisualElement name="clients-list" />
|
||||
</ui:ScrollView>
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement class="button-container">
|
||||
<ui:Button name="skip-clients-button" text="Skip" class="setup-button secondary-button" />
|
||||
<ui:Button name="configure-selected-button" text="Configure Selected" class="setup-button action-button" />
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
</ui:ScrollView>
|
||||
</ui:UXML>
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bf9567b4c9d76a14e9476c2d47c4b017
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
Reference in New Issue
Block a user