升级XR插件版本
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7723ed5eaaccb104e93acb9fd2d8cd32
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,467 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using MCPForUnity.Editor.Constants;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Services;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace MCPForUnity.Editor.Windows.Components.Advanced
|
||||
{
|
||||
/// <summary>
|
||||
/// Controller for the Advanced Settings section.
|
||||
/// Handles path overrides, server source configuration, dev mode, and package deployment.
|
||||
/// </summary>
|
||||
public class McpAdvancedSection
|
||||
{
|
||||
// UI Elements
|
||||
private TextField uvxPathOverride;
|
||||
private Button browseUvxButton;
|
||||
private Button clearUvxButton;
|
||||
private VisualElement uvxPathStatus;
|
||||
private TextField gitUrlOverride;
|
||||
private Button browseGitUrlButton;
|
||||
private Button clearGitUrlButton;
|
||||
private Toggle debugLogsToggle;
|
||||
private Toggle devModeForceRefreshToggle;
|
||||
private Toggle useBetaServerToggle;
|
||||
private TextField deploySourcePath;
|
||||
private Button browseDeploySourceButton;
|
||||
private Button clearDeploySourceButton;
|
||||
private Button deployButton;
|
||||
private Button deployRestoreButton;
|
||||
private Label deployTargetLabel;
|
||||
private Label deployBackupLabel;
|
||||
private Label deployStatusLabel;
|
||||
private VisualElement healthIndicator;
|
||||
private Label healthStatus;
|
||||
private Button testConnectionButton;
|
||||
|
||||
// Events
|
||||
public event Action OnGitUrlChanged;
|
||||
public event Action OnHttpServerCommandUpdateRequested;
|
||||
public event Action OnTestConnectionRequested;
|
||||
public event Action<bool> OnBetaModeChanged;
|
||||
|
||||
public VisualElement Root { get; private set; }
|
||||
|
||||
public McpAdvancedSection(VisualElement root)
|
||||
{
|
||||
Root = root;
|
||||
CacheUIElements();
|
||||
InitializeUI();
|
||||
RegisterCallbacks();
|
||||
}
|
||||
|
||||
private void CacheUIElements()
|
||||
{
|
||||
uvxPathOverride = Root.Q<TextField>("uv-path-override");
|
||||
browseUvxButton = Root.Q<Button>("browse-uv-button");
|
||||
clearUvxButton = Root.Q<Button>("clear-uv-button");
|
||||
uvxPathStatus = Root.Q<VisualElement>("uv-path-status");
|
||||
gitUrlOverride = Root.Q<TextField>("git-url-override");
|
||||
browseGitUrlButton = Root.Q<Button>("browse-git-url-button");
|
||||
clearGitUrlButton = Root.Q<Button>("clear-git-url-button");
|
||||
debugLogsToggle = Root.Q<Toggle>("debug-logs-toggle");
|
||||
devModeForceRefreshToggle = Root.Q<Toggle>("dev-mode-force-refresh-toggle");
|
||||
useBetaServerToggle = Root.Q<Toggle>("use-beta-server-toggle");
|
||||
deploySourcePath = Root.Q<TextField>("deploy-source-path");
|
||||
browseDeploySourceButton = Root.Q<Button>("browse-deploy-source-button");
|
||||
clearDeploySourceButton = Root.Q<Button>("clear-deploy-source-button");
|
||||
deployButton = Root.Q<Button>("deploy-button");
|
||||
deployRestoreButton = Root.Q<Button>("deploy-restore-button");
|
||||
deployTargetLabel = Root.Q<Label>("deploy-target-label");
|
||||
deployBackupLabel = Root.Q<Label>("deploy-backup-label");
|
||||
deployStatusLabel = Root.Q<Label>("deploy-status-label");
|
||||
healthIndicator = Root.Q<VisualElement>("health-indicator");
|
||||
healthStatus = Root.Q<Label>("health-status");
|
||||
testConnectionButton = Root.Q<Button>("test-connection-button");
|
||||
}
|
||||
|
||||
private void InitializeUI()
|
||||
{
|
||||
// Set tooltips for fields
|
||||
if (uvxPathOverride != null)
|
||||
uvxPathOverride.tooltip = "Override path to uvx executable. Leave empty for auto-detection.";
|
||||
if (gitUrlOverride != null)
|
||||
gitUrlOverride.tooltip = "Override server source for uvx --from. Leave empty to use default PyPI package. Example local dev: /path/to/unity-mcp/Server";
|
||||
if (debugLogsToggle != null)
|
||||
{
|
||||
debugLogsToggle.tooltip = "Enable verbose debug logging to the Unity Console.";
|
||||
var debugLabel = debugLogsToggle?.parent?.Q<Label>();
|
||||
if (debugLabel != null)
|
||||
debugLabel.tooltip = debugLogsToggle.tooltip;
|
||||
}
|
||||
if (devModeForceRefreshToggle != null)
|
||||
{
|
||||
devModeForceRefreshToggle.tooltip = "When enabled, generated uvx commands add '--no-cache --refresh' before launching (slower startup, but avoids stale cached builds while iterating on the Server).";
|
||||
var forceRefreshLabel = devModeForceRefreshToggle?.parent?.Q<Label>();
|
||||
if (forceRefreshLabel != null)
|
||||
forceRefreshLabel.tooltip = devModeForceRefreshToggle.tooltip;
|
||||
}
|
||||
if (useBetaServerToggle != null)
|
||||
{
|
||||
useBetaServerToggle.tooltip = "When enabled, uvx will fetch the latest beta server version from PyPI. Enable this on the beta branch to get the matching server version.";
|
||||
var betaServerLabel = useBetaServerToggle?.parent?.Q<Label>();
|
||||
if (betaServerLabel != null)
|
||||
betaServerLabel.tooltip = useBetaServerToggle.tooltip;
|
||||
}
|
||||
if (testConnectionButton != null)
|
||||
testConnectionButton.tooltip = "Test the connection between Unity and the MCP server.";
|
||||
if (deploySourcePath != null)
|
||||
deploySourcePath.tooltip = "Copy a MCPForUnity folder into this project's package location.";
|
||||
|
||||
// Set tooltips for buttons
|
||||
if (browseUvxButton != null)
|
||||
browseUvxButton.tooltip = "Browse for uvx executable";
|
||||
if (clearUvxButton != null)
|
||||
clearUvxButton.tooltip = "Clear override and use auto-detection";
|
||||
if (browseGitUrlButton != null)
|
||||
browseGitUrlButton.tooltip = "Select local server source folder";
|
||||
if (clearGitUrlButton != null)
|
||||
clearGitUrlButton.tooltip = "Clear override and use default PyPI package";
|
||||
if (browseDeploySourceButton != null)
|
||||
browseDeploySourceButton.tooltip = "Select MCPForUnity source folder";
|
||||
if (clearDeploySourceButton != null)
|
||||
clearDeploySourceButton.tooltip = "Clear deployment source path";
|
||||
if (deployButton != null)
|
||||
deployButton.tooltip = "Copy MCPForUnity to this project's package location";
|
||||
if (deployRestoreButton != null)
|
||||
deployRestoreButton.tooltip = "Restore the last backup before deployment";
|
||||
|
||||
gitUrlOverride.value = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");
|
||||
|
||||
bool debugEnabled = EditorPrefs.GetBool(EditorPrefKeys.DebugLogs, false);
|
||||
debugLogsToggle.value = debugEnabled;
|
||||
McpLog.SetDebugLoggingEnabled(debugEnabled);
|
||||
|
||||
devModeForceRefreshToggle.value = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
|
||||
useBetaServerToggle.value = EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, true);
|
||||
UpdatePathOverrides();
|
||||
UpdateDeploymentSection();
|
||||
}
|
||||
|
||||
private void RegisterCallbacks()
|
||||
{
|
||||
browseUvxButton.clicked += OnBrowseUvxClicked;
|
||||
clearUvxButton.clicked += OnClearUvxClicked;
|
||||
browseGitUrlButton.clicked += OnBrowseGitUrlClicked;
|
||||
|
||||
gitUrlOverride.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
string url = evt.newValue?.Trim();
|
||||
if (string.IsNullOrEmpty(url))
|
||||
{
|
||||
EditorPrefs.DeleteKey(EditorPrefKeys.GitUrlOverride);
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorPrefs.SetString(EditorPrefKeys.GitUrlOverride, url);
|
||||
}
|
||||
OnGitUrlChanged?.Invoke();
|
||||
OnHttpServerCommandUpdateRequested?.Invoke();
|
||||
});
|
||||
|
||||
clearGitUrlButton.clicked += () =>
|
||||
{
|
||||
gitUrlOverride.value = string.Empty;
|
||||
EditorPrefs.DeleteKey(EditorPrefKeys.GitUrlOverride);
|
||||
OnGitUrlChanged?.Invoke();
|
||||
OnHttpServerCommandUpdateRequested?.Invoke();
|
||||
};
|
||||
|
||||
debugLogsToggle.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
McpLog.SetDebugLoggingEnabled(evt.newValue);
|
||||
});
|
||||
|
||||
devModeForceRefreshToggle.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
EditorPrefs.SetBool(EditorPrefKeys.DevModeForceServerRefresh, evt.newValue);
|
||||
OnHttpServerCommandUpdateRequested?.Invoke();
|
||||
});
|
||||
|
||||
useBetaServerToggle.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
EditorPrefs.SetBool(EditorPrefKeys.UseBetaServer, evt.newValue);
|
||||
OnHttpServerCommandUpdateRequested?.Invoke();
|
||||
OnBetaModeChanged?.Invoke(evt.newValue);
|
||||
});
|
||||
|
||||
deploySourcePath.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
string path = evt.newValue?.Trim();
|
||||
if (string.IsNullOrEmpty(path) || path == "Not set")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
MCPServiceLocator.Deployment.SetStoredSourcePath(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
EditorUtility.DisplayDialog("Invalid Source", ex.Message, "OK");
|
||||
UpdateDeploymentSection();
|
||||
}
|
||||
});
|
||||
|
||||
browseDeploySourceButton.clicked += OnBrowseDeploySourceClicked;
|
||||
clearDeploySourceButton.clicked += OnClearDeploySourceClicked;
|
||||
deployButton.clicked += OnDeployClicked;
|
||||
deployRestoreButton.clicked += OnRestoreBackupClicked;
|
||||
testConnectionButton.clicked += () => OnTestConnectionRequested?.Invoke();
|
||||
}
|
||||
|
||||
public void UpdatePathOverrides()
|
||||
{
|
||||
var pathService = MCPServiceLocator.Paths;
|
||||
|
||||
bool hasOverride = pathService.HasUvxPathOverride;
|
||||
bool hasFallback = pathService.HasUvxPathFallback;
|
||||
string uvxPath = hasOverride ? pathService.GetUvxPath() : null;
|
||||
|
||||
// Determine display text based on override and fallback status
|
||||
if (hasOverride)
|
||||
{
|
||||
if (hasFallback)
|
||||
{
|
||||
// Override path invalid, using system fallback
|
||||
string overridePath = EditorPrefs.GetString(EditorPrefKeys.UvxPathOverride, string.Empty);
|
||||
uvxPathOverride.value = $"Invalid override path: {overridePath} (fallback to uvx path) {uvxPath}";
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(uvxPath))
|
||||
{
|
||||
// Override path valid
|
||||
uvxPathOverride.value = uvxPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Override set but invalid, no fallback available
|
||||
string overridePath = EditorPrefs.GetString(EditorPrefKeys.UvxPathOverride, string.Empty);
|
||||
uvxPathOverride.value = $"Invalid override path: {overridePath}, no uv found";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
uvxPathOverride.value = "uvx (uses PATH)";
|
||||
}
|
||||
|
||||
uvxPathStatus.RemoveFromClassList("valid");
|
||||
uvxPathStatus.RemoveFromClassList("invalid");
|
||||
uvxPathStatus.RemoveFromClassList("warning");
|
||||
|
||||
if (hasOverride)
|
||||
{
|
||||
if (hasFallback)
|
||||
{
|
||||
// Using fallback - show as warning (yellow)
|
||||
uvxPathStatus.AddToClassList("warning");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Override mode: validate the override path
|
||||
string overridePath = EditorPrefs.GetString(EditorPrefKeys.UvxPathOverride, string.Empty);
|
||||
if (pathService.TryValidateUvxExecutable(overridePath, out _))
|
||||
{
|
||||
uvxPathStatus.AddToClassList("valid");
|
||||
}
|
||||
else
|
||||
{
|
||||
uvxPathStatus.AddToClassList("invalid");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// PATH mode: validate system uvx
|
||||
string systemUvxPath = pathService.GetUvxPath();
|
||||
if (!string.IsNullOrEmpty(systemUvxPath) && pathService.TryValidateUvxExecutable(systemUvxPath, out _))
|
||||
{
|
||||
uvxPathStatus.AddToClassList("valid");
|
||||
}
|
||||
else
|
||||
{
|
||||
uvxPathStatus.AddToClassList("invalid");
|
||||
}
|
||||
}
|
||||
|
||||
gitUrlOverride.value = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");
|
||||
debugLogsToggle.value = EditorPrefs.GetBool(EditorPrefKeys.DebugLogs, false);
|
||||
devModeForceRefreshToggle.value = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
|
||||
useBetaServerToggle.value = EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, true);
|
||||
UpdateDeploymentSection();
|
||||
}
|
||||
|
||||
private void OnBrowseUvxClicked()
|
||||
{
|
||||
string suggested = RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
|
||||
? "/opt/homebrew/bin"
|
||||
: Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
|
||||
string picked = EditorUtility.OpenFilePanel("Select uv Executable", suggested, "");
|
||||
if (!string.IsNullOrEmpty(picked))
|
||||
{
|
||||
try
|
||||
{
|
||||
MCPServiceLocator.Paths.SetUvxPathOverride(picked);
|
||||
UpdatePathOverrides();
|
||||
McpLog.Info($"uv path override set to: {picked}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
EditorUtility.DisplayDialog("Invalid Path", ex.Message, "OK");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnClearUvxClicked()
|
||||
{
|
||||
MCPServiceLocator.Paths.ClearUvxPathOverride();
|
||||
UpdatePathOverrides();
|
||||
McpLog.Info("uv path override cleared");
|
||||
}
|
||||
|
||||
private void OnBrowseGitUrlClicked()
|
||||
{
|
||||
string picked = EditorUtility.OpenFolderPanel("Select Server folder", string.Empty, string.Empty);
|
||||
if (!string.IsNullOrEmpty(picked))
|
||||
{
|
||||
gitUrlOverride.value = picked;
|
||||
EditorPrefs.SetString(EditorPrefKeys.GitUrlOverride, picked);
|
||||
OnGitUrlChanged?.Invoke();
|
||||
OnHttpServerCommandUpdateRequested?.Invoke();
|
||||
McpLog.Info($"Server source override set to: {picked}");
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateDeploymentSection()
|
||||
{
|
||||
var deployService = MCPServiceLocator.Deployment;
|
||||
|
||||
string sourcePath = deployService.GetStoredSourcePath();
|
||||
deploySourcePath.value = sourcePath ?? string.Empty;
|
||||
|
||||
deployTargetLabel.text = $"Target: {deployService.GetTargetDisplayPath()}";
|
||||
|
||||
string backupPath = deployService.GetLastBackupPath();
|
||||
if (deployService.HasBackup())
|
||||
{
|
||||
// Use forward slashes to avoid backslash escape sequence issues in UI text
|
||||
deployBackupLabel.text = $"Last backup: {backupPath?.Replace('\\', '/')}";
|
||||
}
|
||||
else
|
||||
{
|
||||
deployBackupLabel.text = "Last backup: none";
|
||||
}
|
||||
|
||||
deployRestoreButton?.SetEnabled(deployService.HasBackup());
|
||||
}
|
||||
|
||||
private void OnBrowseDeploySourceClicked()
|
||||
{
|
||||
string picked = EditorUtility.OpenFolderPanel("Select MCPForUnity folder", string.Empty, string.Empty);
|
||||
if (string.IsNullOrEmpty(picked))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
MCPServiceLocator.Deployment.SetStoredSourcePath(picked);
|
||||
SetDeployStatus($"Source set: {picked}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
EditorUtility.DisplayDialog("Invalid Source", ex.Message, "OK");
|
||||
SetDeployStatus("Source selection failed");
|
||||
}
|
||||
|
||||
UpdateDeploymentSection();
|
||||
}
|
||||
|
||||
private void OnClearDeploySourceClicked()
|
||||
{
|
||||
MCPServiceLocator.Deployment.ClearStoredSourcePath();
|
||||
UpdateDeploymentSection();
|
||||
SetDeployStatus("Source cleared");
|
||||
}
|
||||
|
||||
private void OnDeployClicked()
|
||||
{
|
||||
var result = MCPServiceLocator.Deployment.DeployFromStoredSource();
|
||||
SetDeployStatus(result.Message, !result.Success);
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
EditorUtility.DisplayDialog("Deployment Failed", result.Message, "OK");
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorUtility.DisplayDialog("Deployment Complete", result.Message + (string.IsNullOrEmpty(result.BackupPath) ? string.Empty : $"\nBackup: {result.BackupPath}"), "OK");
|
||||
}
|
||||
|
||||
UpdateDeploymentSection();
|
||||
}
|
||||
|
||||
private void OnRestoreBackupClicked()
|
||||
{
|
||||
var result = MCPServiceLocator.Deployment.RestoreLastBackup();
|
||||
SetDeployStatus(result.Message, !result.Success);
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
EditorUtility.DisplayDialog("Restore Failed", result.Message, "OK");
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorUtility.DisplayDialog("Restore Complete", result.Message, "OK");
|
||||
}
|
||||
|
||||
UpdateDeploymentSection();
|
||||
}
|
||||
|
||||
private void SetDeployStatus(string message, bool isError = false)
|
||||
{
|
||||
if (deployStatusLabel == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
deployStatusLabel.text = message;
|
||||
deployStatusLabel.style.color = isError
|
||||
? new StyleColor(new Color(0.85f, 0.2f, 0.2f))
|
||||
: StyleKeyword.Null;
|
||||
}
|
||||
|
||||
public void UpdateHealthStatus(bool isHealthy, string statusText)
|
||||
{
|
||||
if (healthStatus != null)
|
||||
{
|
||||
healthStatus.text = statusText;
|
||||
}
|
||||
|
||||
if (healthIndicator != null)
|
||||
{
|
||||
healthIndicator.RemoveFromClassList("healthy");
|
||||
healthIndicator.RemoveFromClassList("disconnected");
|
||||
healthIndicator.RemoveFromClassList("unknown");
|
||||
|
||||
if (isHealthy)
|
||||
{
|
||||
healthIndicator.AddToClassList("healthy");
|
||||
}
|
||||
else if (statusText == HealthStatus.Unknown)
|
||||
{
|
||||
healthIndicator.AddToClassList("unknown");
|
||||
}
|
||||
else
|
||||
{
|
||||
healthIndicator.AddToClassList("disconnected");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bf87d9c1c3b287e4180379f65af95dca
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,66 @@
|
||||
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="True">
|
||||
<Style src="../Common.uss" />
|
||||
<ui:VisualElement name="advanced-section" class="section">
|
||||
<ui:Label text="Advanced Settings" class="section-title" />
|
||||
<ui:VisualElement class="section-content">
|
||||
<ui:VisualElement class="override-row">
|
||||
<ui:Label text="UVX Path:" class="override-label" />
|
||||
<ui:VisualElement class="status-indicator-small" name="uv-path-status" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement class="path-override-controls">
|
||||
<ui:TextField name="uv-path-override" readonly="true" class="override-field" />
|
||||
<ui:Button name="browse-uv-button" text="Browse" class="icon-button" />
|
||||
<ui:Button name="clear-uv-button" text="Clear" class="icon-button" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement class="override-row" style="margin-top: 8px;">
|
||||
<ui:Label text="Server Source:" class="override-label" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement class="path-override-controls">
|
||||
<ui:TextField name="git-url-override" placeholder-text="/path/to/Server or git+https://..." class="override-field" />
|
||||
<ui:Button name="browse-git-url-button" text="Select" class="icon-button" />
|
||||
<ui:Button name="clear-git-url-button" text="Clear" class="icon-button" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement class="setting-row" style="margin-top: 8px;">
|
||||
<ui:Label text="Debug Logging:" class="setting-label" />
|
||||
<ui:Toggle name="debug-logs-toggle" class="setting-toggle" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement class="setting-row">
|
||||
<ui:Label text="Server Health:" class="setting-label" />
|
||||
<ui:VisualElement class="status-container">
|
||||
<ui:VisualElement name="health-indicator" class="status-dot" />
|
||||
<ui:Label name="health-status" text="Unknown" class="status-text" />
|
||||
</ui:VisualElement>
|
||||
<ui:Button name="test-connection-button" text="Test" class="action-button" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement class="setting-row">
|
||||
<ui:Label text="Force Fresh Install:" class="setting-label" />
|
||||
<ui:Toggle name="dev-mode-force-refresh-toggle" class="setting-toggle" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement class="setting-row">
|
||||
<ui:Label text="Use Beta Server:" class="setting-label" />
|
||||
<ui:Toggle name="use-beta-server-toggle" class="setting-toggle" />
|
||||
</ui:VisualElement>
|
||||
|
||||
<ui:VisualElement class="override-row" style="margin-top: 8px;">
|
||||
<ui:Label text="Package Source:" class="override-label" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement class="path-override-controls">
|
||||
<ui:TextField name="deploy-source-path" class="override-field" />
|
||||
<ui:Button name="browse-deploy-source-button" text="Select" class="icon-button" />
|
||||
<ui:Button name="clear-deploy-source-button" text="Clear" class="icon-button" />
|
||||
</ui:VisualElement>
|
||||
<ui:Label name="deploy-target-label" class="help-text" />
|
||||
<ui:Label name="deploy-backup-label" class="help-text" />
|
||||
<ui:VisualElement class="path-override-controls" style="margin-top: 4px;">
|
||||
<ui:Button name="deploy-button" text="Deploy" class="icon-button" />
|
||||
<ui:Button name="deploy-restore-button" text="Restore" class="icon-button" />
|
||||
</ui:VisualElement>
|
||||
<ui:Label name="deploy-status-label" class="help-text" />
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
</ui:UXML>
|
||||
@@ -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: 4d9f5ceeb24166f47804e094440b7846
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,574 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using MCPForUnity.Editor.Clients;
|
||||
using MCPForUnity.Editor.Constants;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Models;
|
||||
using MCPForUnity.Editor.Services;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace MCPForUnity.Editor.Windows.Components.ClientConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Controller for the Client Configuration section of the MCP For Unity editor window.
|
||||
/// Handles client selection, configuration, status display, and manual configuration details.
|
||||
/// </summary>
|
||||
public class McpClientConfigSection
|
||||
{
|
||||
// UI Elements
|
||||
private DropdownField clientDropdown;
|
||||
private Button configureAllButton;
|
||||
private VisualElement clientStatusIndicator;
|
||||
private Label clientStatusLabel;
|
||||
private Button configureButton;
|
||||
private VisualElement claudeCliPathRow;
|
||||
private TextField claudeCliPath;
|
||||
private Button browseClaudeButton;
|
||||
private Foldout manualConfigFoldout;
|
||||
private TextField configPathField;
|
||||
private Button copyPathButton;
|
||||
private Button openFileButton;
|
||||
private TextField configJsonField;
|
||||
private Button copyJsonButton;
|
||||
private Label installationStepsLabel;
|
||||
|
||||
// Data
|
||||
private readonly List<IMcpClientConfigurator> configurators;
|
||||
private readonly Dictionary<IMcpClientConfigurator, DateTime> lastStatusChecks = new();
|
||||
private readonly HashSet<IMcpClientConfigurator> statusRefreshInFlight = new();
|
||||
private static readonly TimeSpan StatusRefreshInterval = TimeSpan.FromSeconds(45);
|
||||
private int selectedClientIndex = 0;
|
||||
|
||||
// Events
|
||||
/// <summary>
|
||||
/// Fired when the selected client's configured transport is detected/updated.
|
||||
/// The parameter contains the client name and its configured transport.
|
||||
/// </summary>
|
||||
public event Action<string, ConfiguredTransport> OnClientTransportDetected;
|
||||
|
||||
public VisualElement Root { get; private set; }
|
||||
|
||||
public McpClientConfigSection(VisualElement root)
|
||||
{
|
||||
Root = root;
|
||||
configurators = MCPServiceLocator.Client.GetAllClients().ToList();
|
||||
CacheUIElements();
|
||||
InitializeUI();
|
||||
RegisterCallbacks();
|
||||
}
|
||||
|
||||
private void CacheUIElements()
|
||||
{
|
||||
clientDropdown = Root.Q<DropdownField>("client-dropdown");
|
||||
configureAllButton = Root.Q<Button>("configure-all-button");
|
||||
clientStatusIndicator = Root.Q<VisualElement>("client-status-indicator");
|
||||
clientStatusLabel = Root.Q<Label>("client-status");
|
||||
configureButton = Root.Q<Button>("configure-button");
|
||||
claudeCliPathRow = Root.Q<VisualElement>("claude-cli-path-row");
|
||||
claudeCliPath = Root.Q<TextField>("claude-cli-path");
|
||||
browseClaudeButton = Root.Q<Button>("browse-claude-button");
|
||||
manualConfigFoldout = Root.Q<Foldout>("manual-config-foldout");
|
||||
configPathField = Root.Q<TextField>("config-path");
|
||||
copyPathButton = Root.Q<Button>("copy-path-button");
|
||||
openFileButton = Root.Q<Button>("open-file-button");
|
||||
configJsonField = Root.Q<TextField>("config-json");
|
||||
copyJsonButton = Root.Q<Button>("copy-json-button");
|
||||
installationStepsLabel = Root.Q<Label>("installation-steps");
|
||||
}
|
||||
|
||||
private void InitializeUI()
|
||||
{
|
||||
// Ensure manual config foldout starts collapsed
|
||||
if (manualConfigFoldout != null)
|
||||
{
|
||||
manualConfigFoldout.value = false;
|
||||
}
|
||||
|
||||
var clientNames = configurators.Select(c => c.DisplayName).ToList();
|
||||
clientDropdown.choices = clientNames;
|
||||
if (clientNames.Count > 0)
|
||||
{
|
||||
clientDropdown.index = 0;
|
||||
}
|
||||
|
||||
claudeCliPathRow.style.display = DisplayStyle.None;
|
||||
|
||||
// Initialize the configuration display for the first selected client
|
||||
UpdateClientStatus();
|
||||
UpdateManualConfiguration();
|
||||
UpdateClaudeCliPathVisibility();
|
||||
}
|
||||
|
||||
private void RegisterCallbacks()
|
||||
{
|
||||
clientDropdown.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
selectedClientIndex = clientDropdown.index;
|
||||
UpdateClientStatus();
|
||||
UpdateManualConfiguration();
|
||||
UpdateClaudeCliPathVisibility();
|
||||
});
|
||||
|
||||
configureAllButton.clicked += OnConfigureAllClientsClicked;
|
||||
configureButton.clicked += OnConfigureClicked;
|
||||
browseClaudeButton.clicked += OnBrowseClaudeClicked;
|
||||
copyPathButton.clicked += OnCopyPathClicked;
|
||||
openFileButton.clicked += OnOpenFileClicked;
|
||||
copyJsonButton.clicked += OnCopyJsonClicked;
|
||||
}
|
||||
|
||||
public void UpdateClientStatus()
|
||||
{
|
||||
if (selectedClientIndex < 0 || selectedClientIndex >= configurators.Count)
|
||||
return;
|
||||
|
||||
var client = configurators[selectedClientIndex];
|
||||
RefreshClientStatus(client);
|
||||
}
|
||||
|
||||
private string GetStatusDisplayString(McpStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
McpStatus.NotConfigured => "Not Configured",
|
||||
McpStatus.Configured => "Configured",
|
||||
McpStatus.Running => "Running",
|
||||
McpStatus.Connected => "Connected",
|
||||
McpStatus.IncorrectPath => "Incorrect Path",
|
||||
McpStatus.CommunicationError => "Communication Error",
|
||||
McpStatus.NoResponse => "No Response",
|
||||
McpStatus.UnsupportedOS => "Unsupported OS",
|
||||
McpStatus.MissingConfig => "Missing MCPForUnity Config",
|
||||
McpStatus.Error => "Error",
|
||||
_ => "Unknown",
|
||||
};
|
||||
}
|
||||
|
||||
public void UpdateManualConfiguration()
|
||||
{
|
||||
if (selectedClientIndex < 0 || selectedClientIndex >= configurators.Count)
|
||||
return;
|
||||
|
||||
var client = configurators[selectedClientIndex];
|
||||
|
||||
string configPath = client.GetConfigPath();
|
||||
configPathField.value = configPath;
|
||||
|
||||
string configJson = client.GetManualSnippet();
|
||||
configJsonField.value = configJson;
|
||||
|
||||
var steps = client.GetInstallationSteps();
|
||||
if (steps != null && steps.Count > 0)
|
||||
{
|
||||
var numbered = steps.Select((s, i) => $"{i + 1}. {s}");
|
||||
installationStepsLabel.text = string.Join("\n", numbered);
|
||||
}
|
||||
else
|
||||
{
|
||||
installationStepsLabel.text = "Configuration steps not available for this client.";
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateClaudeCliPathVisibility()
|
||||
{
|
||||
if (selectedClientIndex < 0 || selectedClientIndex >= configurators.Count)
|
||||
return;
|
||||
|
||||
var client = configurators[selectedClientIndex];
|
||||
|
||||
if (client is ClaudeCliMcpConfigurator)
|
||||
{
|
||||
string claudePath = MCPServiceLocator.Paths.GetClaudeCliPath();
|
||||
if (string.IsNullOrEmpty(claudePath))
|
||||
{
|
||||
claudeCliPathRow.style.display = DisplayStyle.Flex;
|
||||
claudeCliPath.value = "Not found - click Browse to select";
|
||||
}
|
||||
else
|
||||
{
|
||||
claudeCliPathRow.style.display = DisplayStyle.Flex;
|
||||
claudeCliPath.value = claudePath;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
claudeCliPathRow.style.display = DisplayStyle.None;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnConfigureAllClientsClicked()
|
||||
{
|
||||
try
|
||||
{
|
||||
var summary = MCPServiceLocator.Client.ConfigureAllDetectedClients();
|
||||
|
||||
string message = summary.GetSummaryMessage() + "\n\n";
|
||||
foreach (var msg in summary.Messages)
|
||||
{
|
||||
message += msg + "\n";
|
||||
}
|
||||
|
||||
EditorUtility.DisplayDialog("Configure All Clients", message, "OK");
|
||||
|
||||
if (selectedClientIndex >= 0 && selectedClientIndex < configurators.Count)
|
||||
{
|
||||
UpdateClientStatus();
|
||||
UpdateManualConfiguration();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
EditorUtility.DisplayDialog("Configuration Failed", ex.Message, "OK");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnConfigureClicked()
|
||||
{
|
||||
if (selectedClientIndex < 0 || selectedClientIndex >= configurators.Count)
|
||||
return;
|
||||
|
||||
var client = configurators[selectedClientIndex];
|
||||
|
||||
// Handle CLI configurators asynchronously
|
||||
if (client is ClaudeCliMcpConfigurator)
|
||||
{
|
||||
ConfigureClaudeCliAsync(client);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
MCPServiceLocator.Client.ConfigureClient(client);
|
||||
lastStatusChecks.Remove(client);
|
||||
RefreshClientStatus(client, forceImmediate: true);
|
||||
UpdateManualConfiguration();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
clientStatusLabel.text = "Error";
|
||||
clientStatusLabel.style.color = Color.red;
|
||||
McpLog.Error($"Configuration failed: {ex.Message}");
|
||||
EditorUtility.DisplayDialog("Configuration Failed", ex.Message, "OK");
|
||||
}
|
||||
}
|
||||
|
||||
private void ConfigureClaudeCliAsync(IMcpClientConfigurator client)
|
||||
{
|
||||
if (statusRefreshInFlight.Contains(client))
|
||||
return;
|
||||
|
||||
statusRefreshInFlight.Add(client);
|
||||
bool isCurrentlyConfigured = client.Status == McpStatus.Configured;
|
||||
ApplyStatusToUi(client, showChecking: true, customMessage: isCurrentlyConfigured ? "Unregistering..." : "Registering...");
|
||||
|
||||
// Capture ALL main-thread-only values before async task
|
||||
string projectDir = Path.GetDirectoryName(Application.dataPath);
|
||||
bool useHttpTransport = EditorConfigurationCache.Instance.UseHttpTransport;
|
||||
string claudePath = MCPServiceLocator.Paths.GetClaudeCliPath();
|
||||
string httpUrl = HttpEndpointUtility.GetMcpRpcUrl();
|
||||
var (uvxPath, gitUrl, packageName) = AssetPathUtility.GetUvxCommandParts();
|
||||
bool shouldForceRefresh = AssetPathUtility.ShouldForceUvxRefresh();
|
||||
string apiKey = EditorPrefs.GetString(EditorPrefKeys.ApiKey, string.Empty);
|
||||
|
||||
// Compute pathPrepend on main thread
|
||||
string pathPrepend = null;
|
||||
if (Application.platform == RuntimePlatform.OSXEditor)
|
||||
pathPrepend = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
|
||||
else if (Application.platform == RuntimePlatform.LinuxEditor)
|
||||
pathPrepend = "/usr/local/bin:/usr/bin:/bin";
|
||||
try
|
||||
{
|
||||
string claudeDir = Path.GetDirectoryName(claudePath);
|
||||
if (!string.IsNullOrEmpty(claudeDir))
|
||||
pathPrepend = string.IsNullOrEmpty(pathPrepend) ? claudeDir : $"{claudeDir}:{pathPrepend}";
|
||||
}
|
||||
catch { }
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (client is ClaudeCliMcpConfigurator cliConfigurator)
|
||||
{
|
||||
var serverTransport = HttpEndpointUtility.GetCurrentServerTransport();
|
||||
cliConfigurator.ConfigureWithCapturedValues(
|
||||
projectDir, claudePath, pathPrepend,
|
||||
useHttpTransport, httpUrl,
|
||||
uvxPath, gitUrl, packageName, shouldForceRefresh,
|
||||
apiKey, serverTransport);
|
||||
}
|
||||
return (success: true, error: (string)null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (success: false, error: ex.Message);
|
||||
}
|
||||
}).ContinueWith(t =>
|
||||
{
|
||||
string errorMessage = null;
|
||||
if (t.IsFaulted && t.Exception != null)
|
||||
{
|
||||
errorMessage = t.Exception.GetBaseException()?.Message ?? "Configuration failed";
|
||||
}
|
||||
else if (!t.Result.success)
|
||||
{
|
||||
errorMessage = t.Result.error;
|
||||
}
|
||||
|
||||
EditorApplication.delayCall += () =>
|
||||
{
|
||||
statusRefreshInFlight.Remove(client);
|
||||
lastStatusChecks.Remove(client);
|
||||
|
||||
if (errorMessage != null)
|
||||
{
|
||||
if (client is McpClientConfiguratorBase baseConfigurator)
|
||||
{
|
||||
baseConfigurator.Client.SetStatus(McpStatus.Error, errorMessage);
|
||||
}
|
||||
McpLog.Error($"Configuration failed: {errorMessage}");
|
||||
RefreshClientStatus(client, forceImmediate: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Registration succeeded - trust the status set by RegisterWithCapturedValues
|
||||
// and update UI without re-verifying (which could fail due to CLI timing/scope issues)
|
||||
lastStatusChecks[client] = DateTime.UtcNow;
|
||||
ApplyStatusToUi(client);
|
||||
}
|
||||
UpdateManualConfiguration();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private void OnBrowseClaudeClicked()
|
||||
{
|
||||
string suggested = RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
|
||||
? "/opt/homebrew/bin"
|
||||
: Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
|
||||
string picked = EditorUtility.OpenFilePanel("Select Claude CLI", suggested, "");
|
||||
if (!string.IsNullOrEmpty(picked))
|
||||
{
|
||||
try
|
||||
{
|
||||
MCPServiceLocator.Paths.SetClaudeCliPathOverride(picked);
|
||||
UpdateClaudeCliPathVisibility();
|
||||
UpdateClientStatus();
|
||||
McpLog.Info($"Claude CLI path override set to: {picked}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
EditorUtility.DisplayDialog("Invalid Path", ex.Message, "OK");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCopyPathClicked()
|
||||
{
|
||||
EditorGUIUtility.systemCopyBuffer = configPathField.value;
|
||||
McpLog.Info("Config path copied to clipboard");
|
||||
}
|
||||
|
||||
private void OnOpenFileClicked()
|
||||
{
|
||||
string path = configPathField.value;
|
||||
try
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
EditorUtility.DisplayDialog("Open File", "The configuration file path does not exist.", "OK");
|
||||
return;
|
||||
}
|
||||
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = path,
|
||||
UseShellExecute = true
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Error($"Failed to open file: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCopyJsonClicked()
|
||||
{
|
||||
EditorGUIUtility.systemCopyBuffer = configJsonField.value;
|
||||
McpLog.Info("Configuration copied to clipboard");
|
||||
}
|
||||
|
||||
public void RefreshSelectedClient(bool forceImmediate = false)
|
||||
{
|
||||
if (selectedClientIndex >= 0 && selectedClientIndex < configurators.Count)
|
||||
{
|
||||
var client = configurators[selectedClientIndex];
|
||||
// Force immediate for non-Claude CLI, or when explicitly requested
|
||||
bool shouldForceImmediate = forceImmediate || client is not ClaudeCliMcpConfigurator;
|
||||
RefreshClientStatus(client, shouldForceImmediate);
|
||||
UpdateManualConfiguration();
|
||||
UpdateClaudeCliPathVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshClientStatus(IMcpClientConfigurator client, bool forceImmediate = false)
|
||||
{
|
||||
if (client is ClaudeCliMcpConfigurator)
|
||||
{
|
||||
RefreshClaudeCliStatus(client, forceImmediate);
|
||||
return;
|
||||
}
|
||||
|
||||
if (forceImmediate || ShouldRefreshClient(client))
|
||||
{
|
||||
MCPServiceLocator.Client.CheckClientStatus(client);
|
||||
lastStatusChecks[client] = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
ApplyStatusToUi(client);
|
||||
}
|
||||
|
||||
private void RefreshClaudeCliStatus(IMcpClientConfigurator client, bool forceImmediate)
|
||||
{
|
||||
bool hasStatus = lastStatusChecks.ContainsKey(client);
|
||||
bool needsRefresh = !hasStatus || ShouldRefreshClient(client);
|
||||
|
||||
if (!hasStatus)
|
||||
{
|
||||
ApplyStatusToUi(client, showChecking: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplyStatusToUi(client);
|
||||
}
|
||||
|
||||
if ((forceImmediate || needsRefresh) && !statusRefreshInFlight.Contains(client))
|
||||
{
|
||||
statusRefreshInFlight.Add(client);
|
||||
ApplyStatusToUi(client, showChecking: true);
|
||||
|
||||
// Capture main-thread-only values before async task
|
||||
string projectDir = Path.GetDirectoryName(Application.dataPath);
|
||||
bool useHttpTransport = EditorConfigurationCache.Instance.UseHttpTransport;
|
||||
string claudePath = MCPServiceLocator.Paths.GetClaudeCliPath();
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
// Defensive: RefreshClientStatus routes Claude CLI clients here, but avoid hard-cast
|
||||
// so accidental future call sites can't crash the UI.
|
||||
if (client is ClaudeCliMcpConfigurator claudeConfigurator)
|
||||
{
|
||||
// Use thread-safe version with captured main-thread values
|
||||
claudeConfigurator.CheckStatusWithProjectDir(projectDir, useHttpTransport, claudePath, attemptAutoRewrite: false);
|
||||
}
|
||||
}).ContinueWith(t =>
|
||||
{
|
||||
bool faulted = false;
|
||||
string errorMessage = null;
|
||||
if (t.IsFaulted && t.Exception != null)
|
||||
{
|
||||
var baseException = t.Exception.GetBaseException();
|
||||
errorMessage = baseException?.Message ?? "Status check failed";
|
||||
McpLog.Error($"Failed to refresh Claude CLI status: {errorMessage}");
|
||||
faulted = true;
|
||||
}
|
||||
|
||||
EditorApplication.delayCall += () =>
|
||||
{
|
||||
statusRefreshInFlight.Remove(client);
|
||||
lastStatusChecks[client] = DateTime.UtcNow;
|
||||
if (faulted)
|
||||
{
|
||||
if (client is McpClientConfiguratorBase baseConfigurator)
|
||||
{
|
||||
baseConfigurator.Client.SetStatus(McpStatus.Error, errorMessage ?? "Status check failed");
|
||||
}
|
||||
}
|
||||
ApplyStatusToUi(client);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShouldRefreshClient(IMcpClientConfigurator client)
|
||||
{
|
||||
if (!lastStatusChecks.TryGetValue(client, out var last))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return (DateTime.UtcNow - last) > StatusRefreshInterval;
|
||||
}
|
||||
|
||||
private void ApplyStatusToUi(IMcpClientConfigurator client, bool showChecking = false, string customMessage = null)
|
||||
{
|
||||
if (selectedClientIndex < 0 || selectedClientIndex >= configurators.Count)
|
||||
return;
|
||||
|
||||
if (!ReferenceEquals(configurators[selectedClientIndex], client))
|
||||
return;
|
||||
|
||||
clientStatusIndicator.RemoveFromClassList("configured");
|
||||
clientStatusIndicator.RemoveFromClassList("not-configured");
|
||||
clientStatusIndicator.RemoveFromClassList("warning");
|
||||
|
||||
if (showChecking)
|
||||
{
|
||||
clientStatusLabel.text = customMessage ?? "Checking...";
|
||||
clientStatusLabel.style.color = StyleKeyword.Null;
|
||||
clientStatusIndicator.AddToClassList("warning");
|
||||
configureButton.text = client.GetConfigureActionLabel();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for transport mismatch (3-way: Stdio, Http, HttpRemote)
|
||||
bool hasTransportMismatch = false;
|
||||
if (client.ConfiguredTransport != ConfiguredTransport.Unknown)
|
||||
{
|
||||
ConfiguredTransport serverTransport = HttpEndpointUtility.GetCurrentServerTransport();
|
||||
hasTransportMismatch = client.ConfiguredTransport != serverTransport;
|
||||
}
|
||||
|
||||
// If configured but with transport mismatch, show warning state
|
||||
if (hasTransportMismatch && (client.Status == McpStatus.Configured || client.Status == McpStatus.Running || client.Status == McpStatus.Connected))
|
||||
{
|
||||
clientStatusLabel.text = "Transport Mismatch";
|
||||
clientStatusIndicator.AddToClassList("warning");
|
||||
}
|
||||
else
|
||||
{
|
||||
clientStatusLabel.text = GetStatusDisplayString(client.Status);
|
||||
|
||||
switch (client.Status)
|
||||
{
|
||||
case McpStatus.Configured:
|
||||
case McpStatus.Running:
|
||||
case McpStatus.Connected:
|
||||
clientStatusIndicator.AddToClassList("configured");
|
||||
break;
|
||||
case McpStatus.IncorrectPath:
|
||||
case McpStatus.CommunicationError:
|
||||
case McpStatus.NoResponse:
|
||||
clientStatusIndicator.AddToClassList("warning");
|
||||
break;
|
||||
default:
|
||||
clientStatusIndicator.AddToClassList("not-configured");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
clientStatusLabel.style.color = StyleKeyword.Null;
|
||||
configureButton.text = client.GetConfigureActionLabel();
|
||||
|
||||
// Notify listeners about the client's configured transport
|
||||
OnClientTransportDetected?.Invoke(client.DisplayName, client.ConfiguredTransport);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e29d88664440184e9c0165aadf02d46
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,42 @@
|
||||
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="True">
|
||||
<Style src="../Common.uss" />
|
||||
<ui:VisualElement name="client-section" class="section">
|
||||
<ui:Label text="Client Configuration" class="section-title" />
|
||||
<ui:VisualElement class="section-content">
|
||||
<ui:VisualElement class="setting-row">
|
||||
<ui:Label text="Client:" class="setting-label" />
|
||||
<ui:DropdownField name="client-dropdown" class="setting-dropdown-inline" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement class="setting-row">
|
||||
<ui:VisualElement class="status-container">
|
||||
<ui:VisualElement name="client-status-indicator" class="status-dot" />
|
||||
<ui:Label name="client-status" text="Not Configured" class="status-text" />
|
||||
</ui:VisualElement>
|
||||
<ui:Button name="configure-button" text="Configure" class="action-button" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement class="setting-row" name="claude-cli-path-row" style="display: none;">
|
||||
<ui:Label text="Claude CLI Path:" class="setting-label-small" />
|
||||
<ui:TextField name="claude-cli-path" readonly="true" class="path-display-field" />
|
||||
<ui:Button name="browse-claude-button" text="Browse" class="icon-button" />
|
||||
</ui:VisualElement>
|
||||
<ui:Foldout name="manual-config-foldout" text="Manual Configuration" value="false" class="manual-config-foldout">
|
||||
<ui:VisualElement class="manual-config-content">
|
||||
<ui:Label text="Config Path:" class="config-label" />
|
||||
<ui:VisualElement class="path-row">
|
||||
<ui:TextField name="config-path" readonly="true" class="config-path-field" />
|
||||
<ui:Button name="copy-path-button" text="Copy" class="icon-button" />
|
||||
<ui:Button name="open-file-button" text="Open" class="icon-button" />
|
||||
</ui:VisualElement>
|
||||
<ui:Label text="Configuration:" class="config-label" />
|
||||
<ui:VisualElement class="config-json-row">
|
||||
<ui:TextField name="config-json" readonly="true" multiline="true" class="config-json-field" />
|
||||
<ui:Button name="copy-json-button" text="Copy" class="icon-button-vertical" />
|
||||
</ui:VisualElement>
|
||||
<ui:Label text="Installation Steps:" class="config-label" />
|
||||
<ui:Label name="installation-steps" class="installation-steps" />
|
||||
</ui:VisualElement>
|
||||
</ui:Foldout>
|
||||
<ui:Button name="configure-all-button" text="Configure All Detected Clients" class="secondary-button" style="width: auto; padding-left: 12px; padding-right: 12px;" />
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
</ui:UXML>
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: abdb7edaa375af049bd795c7a8b8a613
|
||||
ScriptedImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
|
||||
615
Packages/MCPForUnity/Editor/Windows/Components/Common.uss
Normal file
615
Packages/MCPForUnity/Editor/Windows/Components/Common.uss
Normal file
@@ -0,0 +1,615 @@
|
||||
/* Root Container */
|
||||
#root-container {
|
||||
padding: 16px;
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Title */
|
||||
.title {
|
||||
font-size: 20px;
|
||||
-unity-font-style: bold;
|
||||
margin-bottom: 20px;
|
||||
padding: 8px;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Section Styling */
|
||||
.section {
|
||||
margin-bottom: 14px;
|
||||
padding: 8px;
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
border-radius: 4px;
|
||||
border-width: 1px;
|
||||
border-color: rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Remove bottom margin from last section in a stack */
|
||||
/* Note: Unity UI Toolkit doesn't support :last-child pseudo-class.
|
||||
The .section-last class is applied programmatically instead. */
|
||||
.section-stack > .section.section-last {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
-unity-font-style: bold;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 6px;
|
||||
letter-spacing: 0.3px;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.section-content {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
/* Setting Rows */
|
||||
.setting-row {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
min-height: 24px;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.setting-column {
|
||||
flex-direction: column;
|
||||
margin-bottom: 4px;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
min-width: 140px;
|
||||
flex-shrink: 0;
|
||||
-unity-text-align: middle-left;
|
||||
}
|
||||
|
||||
.setting-value {
|
||||
-unity-text-align: middle-right;
|
||||
color: rgba(150, 150, 150, 1);
|
||||
}
|
||||
|
||||
.setting-toggle {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.setting-dropdown {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
margin-top: 4px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.setting-dropdown-inline {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-width: 150px;
|
||||
flex-basis: 200px;
|
||||
}
|
||||
|
||||
/* Validation Description */
|
||||
.validation-description {
|
||||
margin-top: 4px;
|
||||
padding: 8px;
|
||||
background-color: rgba(100, 150, 200, 0.15);
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
/* Port Fields */
|
||||
.port-field {
|
||||
width: 100px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* URL Fields */
|
||||
.url-field {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-width: 200px;
|
||||
flex-basis: 250px;
|
||||
}
|
||||
|
||||
.port-field > .unity-text-field__input {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
-unity-text-align: middle-center;
|
||||
}
|
||||
|
||||
/* Status Container */
|
||||
.status-container {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 6px;
|
||||
margin-right: 8px;
|
||||
background-color: rgba(150, 150, 150, 1);
|
||||
}
|
||||
|
||||
.status-dot.connected {
|
||||
background-color: rgba(0, 200, 100, 1);
|
||||
}
|
||||
|
||||
.status-dot.disconnected {
|
||||
background-color: rgba(200, 50, 50, 1);
|
||||
}
|
||||
|
||||
.status-dot.configured {
|
||||
background-color: rgba(0, 200, 100, 1);
|
||||
}
|
||||
|
||||
.status-dot.not-configured {
|
||||
background-color: rgba(200, 50, 50, 1);
|
||||
}
|
||||
|
||||
.status-dot.warning {
|
||||
background-color: rgba(255, 200, 0, 1);
|
||||
}
|
||||
|
||||
.status-dot.unknown {
|
||||
background-color: rgba(150, 150, 150, 1);
|
||||
}
|
||||
|
||||
.status-dot.healthy {
|
||||
background-color: rgba(0, 200, 100, 1);
|
||||
}
|
||||
|
||||
.status-text {
|
||||
-unity-text-align: middle-left;
|
||||
}
|
||||
|
||||
.status-indicator-small {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
margin-left: 8px;
|
||||
background-color: rgba(150, 150, 150, 1);
|
||||
}
|
||||
|
||||
.status-indicator-small.valid {
|
||||
background-color: rgba(0, 200, 100, 1);
|
||||
}
|
||||
|
||||
.status-indicator-small.invalid {
|
||||
background-color: rgba(200, 50, 50, 1);
|
||||
}
|
||||
|
||||
.status-indicator-small.warning {
|
||||
background-color: rgba(255, 200, 0, 1);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.action-button {
|
||||
min-width: 80px;
|
||||
height: 28px;
|
||||
margin-left: 8px;
|
||||
background-color: rgba(50, 150, 250, 0.8);
|
||||
border-radius: 4px;
|
||||
-unity-font-style: bold;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
background-color: rgba(50, 150, 250, 1);
|
||||
}
|
||||
|
||||
.action-button:active {
|
||||
background-color: rgba(30, 120, 200, 1);
|
||||
}
|
||||
|
||||
/* Start Server button in the manual config section should align flush left like other full-width controls */
|
||||
.start-server-button {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
/* When the HTTP server/session is running, we show the Start/Stop button as "danger" (red) */
|
||||
.action-button.server-running {
|
||||
background-color: rgba(200, 50, 50, 0.85);
|
||||
}
|
||||
|
||||
.action-button.server-running:hover {
|
||||
background-color: rgba(220, 60, 60, 1);
|
||||
}
|
||||
|
||||
.action-button.server-running:active {
|
||||
background-color: rgba(170, 40, 40, 1);
|
||||
}
|
||||
|
||||
.secondary-button {
|
||||
width: 100%;
|
||||
height: 28px;
|
||||
background-color: rgba(100, 100, 100, 0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.secondary-button:hover {
|
||||
background-color: rgba(100, 100, 100, 0.5);
|
||||
}
|
||||
|
||||
.secondary-button:active {
|
||||
background-color: rgba(80, 80, 80, 0.5);
|
||||
}
|
||||
|
||||
/* Manual Configuration */
|
||||
.manual-config-content {
|
||||
padding: 8px;
|
||||
margin-top: 8px;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 4px;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.config-label {
|
||||
font-size: 11px;
|
||||
-unity-font-style: bold;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.path-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.config-path-field {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
margin-right: 4px;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.config-path-field > .unity-text-field__input {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
font-size: 11px;
|
||||
overflow: hidden;
|
||||
text-overflow: clip;
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
min-width: 50px;
|
||||
height: 24px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.config-json-row {
|
||||
flex-direction: row;
|
||||
margin-bottom: 8px;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.config-json-field {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-height: 64px;
|
||||
margin-right: 4px;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.config-json-field > .unity-text-field__input {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
font-size: 10px;
|
||||
-unity-font-style: normal;
|
||||
white-space: normal;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.icon-button-vertical {
|
||||
min-width: 50px;
|
||||
height: 30px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.installation-steps {
|
||||
padding: 8px;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
white-space: normal;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Tools Section */
|
||||
.tool-actions {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tool-action-button {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
height: 26px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.tool-category-container {
|
||||
flex-direction: column;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.tool-item {
|
||||
flex-direction: column;
|
||||
padding: 8px;
|
||||
margin-bottom: 8px;
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
border-radius: 4px;
|
||||
border-width: 1px;
|
||||
border-color: rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.tool-item-header {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tool-item-toggle {
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tool-tags {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
margin-left: auto;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.tool-tag {
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
margin-left: 4px;
|
||||
margin-top: 2px;
|
||||
background-color: rgba(100, 100, 100, 0.25);
|
||||
border-radius: 3px;
|
||||
color: rgba(40, 40, 40, 1);
|
||||
}
|
||||
|
||||
.tool-item-description,
|
||||
.tool-parameters {
|
||||
font-size: 11px;
|
||||
color: rgba(120, 120, 120, 1);
|
||||
white-space: normal;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.tool-parameters {
|
||||
-unity-font-style: italic;
|
||||
}
|
||||
|
||||
/* Advanced Settings */
|
||||
.advanced-settings-foldout {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.advanced-settings-foldout > .unity-foldout__toggle {
|
||||
-unity-font-style: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Manual Command Foldout */
|
||||
.manual-command-foldout {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.manual-command-foldout > .unity-foldout__toggle {
|
||||
font-size: 11px;
|
||||
padding: 4px;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.manual-command-foldout > .unity-foldout__content {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.advanced-settings-content {
|
||||
padding: 8px;
|
||||
margin-top: 8px;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 4px;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.advanced-label {
|
||||
font-size: 11px;
|
||||
-unity-font-style: bold;
|
||||
margin-bottom: 8px;
|
||||
color: rgba(150, 150, 150, 1);
|
||||
white-space: normal;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.override-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.override-label {
|
||||
font-size: 11px;
|
||||
-unity-font-style: bold;
|
||||
min-width: 120px;
|
||||
white-space: normal;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
font-size: 10px;
|
||||
color: rgba(120, 120, 120, 1);
|
||||
white-space: normal;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.help-text.http-local-url-error {
|
||||
color: rgba(255, 80, 80, 1);
|
||||
-unity-font-style: bold;
|
||||
}
|
||||
|
||||
.path-override-controls {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.override-field {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
margin-right: 4px;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.override-field > .unity-text-field__input {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
font-size: 11px;
|
||||
overflow: hidden;
|
||||
text-overflow: clip;
|
||||
}
|
||||
|
||||
.setting-label-small {
|
||||
font-size: 11px;
|
||||
min-width: 120px;
|
||||
-unity-text-align: middle-left;
|
||||
}
|
||||
|
||||
.path-display-field {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
margin-right: 4px;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.path-display-field > .unity-text-field__input {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
font-size: 11px;
|
||||
overflow: hidden;
|
||||
text-overflow: clip;
|
||||
}
|
||||
|
||||
/* Light Theme Overrides */
|
||||
.unity-theme-light .title {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.unity-theme-light .section {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
border-color: rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.unity-theme-light .section-title {
|
||||
border-bottom-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.unity-theme-dark .tool-tag {
|
||||
color: rgba(220, 220, 220, 1);
|
||||
background-color: rgba(80, 80, 80, 0.6);
|
||||
}
|
||||
|
||||
.unity-theme-dark .tool-item {
|
||||
background-color: rgba(255, 255, 255, 0.04);
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.unity-theme-dark .tool-item-description,
|
||||
.unity-theme-dark .tool-parameters {
|
||||
color: rgba(200, 200, 200, 0.8);
|
||||
}
|
||||
|
||||
|
||||
.unity-theme-light .validation-description {
|
||||
background-color: rgba(100, 150, 200, 0.1);
|
||||
}
|
||||
|
||||
.unity-theme-light .port-field > .unity-text-field__input {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.unity-theme-light .config-path-field > .unity-text-field__input {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.unity-theme-light .config-json-field > .unity-text-field__input {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.unity-theme-light .manual-config-content {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.unity-theme-light .installation-steps {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.unity-theme-light .advanced-settings-content {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.unity-theme-light .override-field > .unity-text-field__input {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.unity-theme-light .path-display-field > .unity-text-field__input {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Warning Banner (for transport mismatch, etc.) */
|
||||
.warning-banner {
|
||||
display: none;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 6px;
|
||||
background-color: rgba(255, 180, 0, 0.2);
|
||||
border-radius: 4px;
|
||||
border-width: 1px;
|
||||
border-color: rgba(255, 180, 0, 0.5);
|
||||
}
|
||||
|
||||
.warning-banner.visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.warning-banner-text {
|
||||
font-size: 11px;
|
||||
white-space: normal;
|
||||
color: rgba(180, 120, 0, 1);
|
||||
}
|
||||
|
||||
.unity-theme-dark .warning-banner {
|
||||
background-color: rgba(255, 180, 0, 0.15);
|
||||
border-color: rgba(255, 180, 0, 0.4);
|
||||
}
|
||||
|
||||
.unity-theme-dark .warning-banner-text {
|
||||
color: rgba(255, 200, 100, 1);
|
||||
}
|
||||
|
||||
.unity-theme-light .manual-command-foldout > .unity-foldout__toggle {
|
||||
background-color: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
@@ -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,54 @@
|
||||
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="True">
|
||||
<Style src="../Common.uss" />
|
||||
<ui:VisualElement name="connection-section" class="section">
|
||||
<ui:Label text="Server" class="section-title" />
|
||||
<ui:VisualElement class="section-content">
|
||||
<ui:VisualElement class="setting-row">
|
||||
<ui:Label text="Transport:" class="setting-label" />
|
||||
<uie:EnumField name="transport-dropdown" class="setting-dropdown-inline" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement name="transport-mismatch-warning" class="warning-banner">
|
||||
<ui:Label name="transport-mismatch-text" class="warning-banner-text" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement class="setting-row" name="http-url-row">
|
||||
<ui:Label text="HTTP URL:" class="setting-label" />
|
||||
<ui:TextField name="http-url" class="url-field" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement name="api-key-row" style="margin-bottom: 4px;">
|
||||
<ui:VisualElement class="setting-row">
|
||||
<ui:Label text="API Key:" class="setting-label" />
|
||||
<ui:TextField name="api-key-field" password="true" class="url-field" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement style="flex-direction: row; justify-content: flex-end;">
|
||||
<ui:Button name="get-api-key-button" text="Get API Key" class="action-button" />
|
||||
<ui:Button name="clear-api-key-button" text="Clear" class="action-button" />
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement class="setting-row" name="http-server-control-row">
|
||||
<ui:Label text="Local Server:" class="setting-label" />
|
||||
<ui:Button name="start-http-server-button" text="Start Server" class="action-button start-server-button" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement class="setting-row" name="unity-socket-port-row">
|
||||
<ui:Label text="Unity Socket Port:" class="setting-label" />
|
||||
<ui:TextField name="unity-port" class="port-field" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement class="setting-row">
|
||||
<ui:VisualElement class="status-container">
|
||||
<ui:VisualElement name="status-indicator" class="status-dot" />
|
||||
<ui:Label name="connection-status" text="Disconnected" class="status-text" />
|
||||
</ui:VisualElement>
|
||||
<ui:Button name="connection-toggle" text="Start" class="action-button" />
|
||||
</ui:VisualElement>
|
||||
<ui:Foldout name="manual-command-foldout" text="Manual Server Launch" value="false" class="manual-config-foldout">
|
||||
<ui:VisualElement name="http-server-command-section" class="manual-config-content">
|
||||
<ui:Label text="Use this command to launch the server manually:" class="config-label" />
|
||||
<ui:VisualElement class="config-json-row">
|
||||
<ui:TextField name="http-server-command" readonly="true" multiline="true" class="config-json-field" />
|
||||
<ui:Button name="copy-http-server-command-button" text="Copy" class="icon-button-vertical" />
|
||||
</ui:VisualElement>
|
||||
<ui:Label name="http-server-command-hint" class="help-text" />
|
||||
</ui:VisualElement>
|
||||
</ui:Foldout>
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
</ui:UXML>
|
||||
@@ -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,15 @@
|
||||
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="True">
|
||||
<ui:VisualElement name="resources-section" class="section">
|
||||
<ui:Label text="Resources" class="section-title" />
|
||||
<ui:VisualElement class="section-content">
|
||||
<ui:Label name="resources-summary" class="help-text" text="Discovering resources..." />
|
||||
<ui:VisualElement name="resources-actions" class="tool-actions">
|
||||
<ui:Button name="enable-all-resources-button" text="Enable All" class="tool-action-button" />
|
||||
<ui:Button name="disable-all-resources-button" text="Disable All" class="tool-action-button" />
|
||||
<ui:Button name="rescan-resources-button" text="Rescan" class="tool-action-button" />
|
||||
</ui:VisualElement>
|
||||
<ui:Label name="resources-note" class="help-text" text="Changes apply after reconnecting or re-registering resources." />
|
||||
<ui:VisualElement name="resource-category-container" class="tool-category-container" />
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
</ui:UXML>
|
||||
@@ -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,334 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MCPForUnity.Editor.Constants;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Services;
|
||||
using MCPForUnity.Editor.Tools;
|
||||
using UnityEditor;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace MCPForUnity.Editor.Windows.Components.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Controller for the Tools section inside the MCP For Unity editor window.
|
||||
/// Provides discovery, filtering, and per-tool enablement toggles.
|
||||
/// </summary>
|
||||
public class McpToolsSection
|
||||
{
|
||||
private readonly Dictionary<string, Toggle> toolToggleMap = new();
|
||||
private Toggle projectScopedToolsToggle;
|
||||
private Label summaryLabel;
|
||||
private Label noteLabel;
|
||||
private Button enableAllButton;
|
||||
private Button disableAllButton;
|
||||
private Button rescanButton;
|
||||
private VisualElement categoryContainer;
|
||||
private List<ToolMetadata> allTools = new();
|
||||
|
||||
public VisualElement Root { get; }
|
||||
|
||||
public McpToolsSection(VisualElement root)
|
||||
{
|
||||
Root = root;
|
||||
CacheUIElements();
|
||||
RegisterCallbacks();
|
||||
}
|
||||
|
||||
private void CacheUIElements()
|
||||
{
|
||||
projectScopedToolsToggle = Root.Q<Toggle>("project-scoped-tools-toggle");
|
||||
summaryLabel = Root.Q<Label>("tools-summary");
|
||||
noteLabel = Root.Q<Label>("tools-note");
|
||||
enableAllButton = Root.Q<Button>("enable-all-button");
|
||||
disableAllButton = Root.Q<Button>("disable-all-button");
|
||||
rescanButton = Root.Q<Button>("rescan-button");
|
||||
categoryContainer = Root.Q<VisualElement>("tool-category-container");
|
||||
}
|
||||
|
||||
private void RegisterCallbacks()
|
||||
{
|
||||
if (projectScopedToolsToggle != null)
|
||||
{
|
||||
projectScopedToolsToggle.value = EditorPrefs.GetBool(
|
||||
EditorPrefKeys.ProjectScopedToolsLocalHttp,
|
||||
false
|
||||
);
|
||||
projectScopedToolsToggle.tooltip = "When enabled, register project-scoped tools with HTTP Local transport. Allows per-project tool customization.";
|
||||
projectScopedToolsToggle.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
EditorPrefs.SetBool(EditorPrefKeys.ProjectScopedToolsLocalHttp, evt.newValue);
|
||||
});
|
||||
}
|
||||
|
||||
if (enableAllButton != null)
|
||||
{
|
||||
enableAllButton.AddToClassList("tool-action-button");
|
||||
enableAllButton.style.marginRight = 4;
|
||||
enableAllButton.clicked += () => SetAllToolsState(true);
|
||||
}
|
||||
|
||||
if (disableAllButton != null)
|
||||
{
|
||||
disableAllButton.AddToClassList("tool-action-button");
|
||||
disableAllButton.style.marginRight = 4;
|
||||
disableAllButton.clicked += () => SetAllToolsState(false);
|
||||
}
|
||||
|
||||
if (rescanButton != null)
|
||||
{
|
||||
rescanButton.AddToClassList("tool-action-button");
|
||||
rescanButton.clicked += () =>
|
||||
{
|
||||
McpLog.Info("Rescanning MCP tools from the editor window.");
|
||||
MCPServiceLocator.ToolDiscovery.InvalidateCache();
|
||||
Refresh();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds the tool list and synchronises toggle states.
|
||||
/// </summary>
|
||||
public void Refresh()
|
||||
{
|
||||
toolToggleMap.Clear();
|
||||
categoryContainer?.Clear();
|
||||
|
||||
var service = MCPServiceLocator.ToolDiscovery;
|
||||
allTools = service.DiscoverAllTools()
|
||||
.OrderBy(tool => IsBuiltIn(tool) ? 0 : 1)
|
||||
.ThenBy(tool => tool.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
bool hasTools = allTools.Count > 0;
|
||||
enableAllButton?.SetEnabled(hasTools);
|
||||
disableAllButton?.SetEnabled(hasTools);
|
||||
|
||||
if (noteLabel != null)
|
||||
{
|
||||
noteLabel.style.display = hasTools ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
}
|
||||
|
||||
if (!hasTools)
|
||||
{
|
||||
AddInfoLabel("No MCP tools found. Add classes decorated with [McpForUnityTool] to expose tools.");
|
||||
UpdateSummary();
|
||||
return;
|
||||
}
|
||||
|
||||
BuildCategory("Built-in Tools", "built-in", allTools.Where(IsBuiltIn));
|
||||
|
||||
var customTools = allTools.Where(tool => !IsBuiltIn(tool)).ToList();
|
||||
if (customTools.Count > 0)
|
||||
{
|
||||
BuildCategory("Custom Tools", "custom", customTools);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddInfoLabel("No custom tools detected in loaded assemblies.");
|
||||
}
|
||||
|
||||
UpdateSummary();
|
||||
}
|
||||
|
||||
private void BuildCategory(string title, string prefsSuffix, IEnumerable<ToolMetadata> tools)
|
||||
{
|
||||
var toolList = tools.ToList();
|
||||
if (toolList.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var foldout = new Foldout
|
||||
{
|
||||
text = $"{title} ({toolList.Count})",
|
||||
value = EditorPrefs.GetBool(EditorPrefKeys.ToolFoldoutStatePrefix + prefsSuffix, true)
|
||||
};
|
||||
|
||||
foldout.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
EditorPrefs.SetBool(EditorPrefKeys.ToolFoldoutStatePrefix + prefsSuffix, evt.newValue);
|
||||
});
|
||||
|
||||
foreach (var tool in toolList)
|
||||
{
|
||||
foldout.Add(CreateToolRow(tool));
|
||||
}
|
||||
|
||||
categoryContainer?.Add(foldout);
|
||||
}
|
||||
|
||||
private VisualElement CreateToolRow(ToolMetadata tool)
|
||||
{
|
||||
var row = new VisualElement();
|
||||
row.AddToClassList("tool-item");
|
||||
|
||||
var header = new VisualElement();
|
||||
header.AddToClassList("tool-item-header");
|
||||
|
||||
var toggle = new Toggle(tool.Name)
|
||||
{
|
||||
value = MCPServiceLocator.ToolDiscovery.IsToolEnabled(tool.Name)
|
||||
};
|
||||
toggle.AddToClassList("tool-item-toggle");
|
||||
toggle.tooltip = string.IsNullOrWhiteSpace(tool.Description) ? tool.Name : tool.Description;
|
||||
|
||||
toggle.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
HandleToggleChange(tool, evt.newValue);
|
||||
});
|
||||
|
||||
toolToggleMap[tool.Name] = toggle;
|
||||
header.Add(toggle);
|
||||
|
||||
var tagsContainer = new VisualElement();
|
||||
tagsContainer.AddToClassList("tool-tags");
|
||||
|
||||
bool defaultEnabled = tool.AutoRegister || tool.IsBuiltIn;
|
||||
tagsContainer.Add(CreateTag(defaultEnabled ? "On by default" : "Off by default"));
|
||||
|
||||
tagsContainer.Add(CreateTag(tool.StructuredOutput ? "Structured output" : "Free-form"));
|
||||
|
||||
if (tool.RequiresPolling)
|
||||
{
|
||||
tagsContainer.Add(CreateTag($"Polling: {tool.PollAction}"));
|
||||
}
|
||||
|
||||
header.Add(tagsContainer);
|
||||
row.Add(header);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(tool.Description))
|
||||
{
|
||||
var description = new Label(tool.Description);
|
||||
description.AddToClassList("tool-item-description");
|
||||
row.Add(description);
|
||||
}
|
||||
|
||||
if (tool.Parameters != null && tool.Parameters.Count > 0)
|
||||
{
|
||||
var paramSummary = string.Join(", ", tool.Parameters.Select(p =>
|
||||
$"{p.Name}{(p.Required ? string.Empty : " (optional)")}: {p.Type}"));
|
||||
|
||||
var parametersLabel = new Label(paramSummary);
|
||||
parametersLabel.AddToClassList("tool-parameters");
|
||||
row.Add(parametersLabel);
|
||||
}
|
||||
|
||||
if (IsManageSceneTool(tool))
|
||||
{
|
||||
row.Add(CreateManageSceneActions());
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
private void HandleToggleChange(ToolMetadata tool, bool enabled, bool updateSummary = true)
|
||||
{
|
||||
MCPServiceLocator.ToolDiscovery.SetToolEnabled(tool.Name, enabled);
|
||||
|
||||
if (updateSummary)
|
||||
{
|
||||
UpdateSummary();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetAllToolsState(bool enabled)
|
||||
{
|
||||
foreach (var tool in allTools)
|
||||
{
|
||||
if (!toolToggleMap.TryGetValue(tool.Name, out var toggle))
|
||||
{
|
||||
MCPServiceLocator.ToolDiscovery.SetToolEnabled(tool.Name, enabled);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (toggle.value == enabled)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
toggle.SetValueWithoutNotify(enabled);
|
||||
HandleToggleChange(tool, enabled, updateSummary: false);
|
||||
}
|
||||
|
||||
UpdateSummary();
|
||||
}
|
||||
|
||||
private void UpdateSummary()
|
||||
{
|
||||
if (summaryLabel == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (allTools.Count == 0)
|
||||
{
|
||||
summaryLabel.text = "No MCP tools discovered.";
|
||||
return;
|
||||
}
|
||||
|
||||
int enabledCount = allTools.Count(tool => MCPServiceLocator.ToolDiscovery.IsToolEnabled(tool.Name));
|
||||
summaryLabel.text = $"{enabledCount} of {allTools.Count} tools will register with connected clients.";
|
||||
}
|
||||
|
||||
private void AddInfoLabel(string message)
|
||||
{
|
||||
var label = new Label(message);
|
||||
label.AddToClassList("help-text");
|
||||
categoryContainer?.Add(label);
|
||||
}
|
||||
|
||||
private VisualElement CreateManageSceneActions()
|
||||
{
|
||||
var actions = new VisualElement();
|
||||
actions.AddToClassList("tool-item-actions");
|
||||
|
||||
var screenshotButton = new Button(OnManageSceneScreenshotClicked)
|
||||
{
|
||||
text = "Capture Screenshot"
|
||||
};
|
||||
screenshotButton.AddToClassList("tool-action-button");
|
||||
screenshotButton.style.marginTop = 4;
|
||||
screenshotButton.tooltip = "Capture a screenshot to Assets/Screenshots via manage_scene.";
|
||||
|
||||
actions.Add(screenshotButton);
|
||||
return actions;
|
||||
}
|
||||
|
||||
private void OnManageSceneScreenshotClicked()
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = ManageScene.ExecuteScreenshot();
|
||||
if (response is SuccessResponse success && !string.IsNullOrWhiteSpace(success.Message))
|
||||
{
|
||||
McpLog.Info(success.Message);
|
||||
}
|
||||
else if (response is ErrorResponse error && !string.IsNullOrWhiteSpace(error.Error))
|
||||
{
|
||||
McpLog.Error(error.Error);
|
||||
}
|
||||
else
|
||||
{
|
||||
McpLog.Info("Screenshot capture requested.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Error($"Failed to capture screenshot: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static Label CreateTag(string text)
|
||||
{
|
||||
var tag = new Label(text);
|
||||
tag.AddToClassList("tool-tag");
|
||||
return tag;
|
||||
}
|
||||
|
||||
private static bool IsManageSceneTool(ToolMetadata tool) => string.Equals(tool?.Name, "manage_scene", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsBuiltIn(ToolMetadata tool) => tool?.IsBuiltIn ?? false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c6b3eaf7efb642e89b9b9548458f72d6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,19 @@
|
||||
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" editor-extension-mode="True">
|
||||
<ui:VisualElement name="tools-section" class="section">
|
||||
<ui:Label text="Tools" class="section-title" />
|
||||
<ui:VisualElement class="section-content">
|
||||
<ui:VisualElement class="setting-row" name="project-scoped-tools-row">
|
||||
<ui:Label text="Project-Scoped Tools:" class="setting-label" />
|
||||
<ui:Toggle name="project-scoped-tools-toggle" />
|
||||
</ui:VisualElement>
|
||||
<ui:Label name="tools-summary" class="help-text" text="Discovering tools..." />
|
||||
<ui:VisualElement name="tools-actions" class="tool-actions">
|
||||
<ui:Button name="enable-all-button" text="Enable All" class="tool-action-button" />
|
||||
<ui:Button name="disable-all-button" text="Disable All" class="tool-action-button" />
|
||||
<ui:Button name="rescan-button" text="Rescan" class="tool-action-button" />
|
||||
</ui:VisualElement>
|
||||
<ui:Label name="tools-note" class="help-text" text="Changes apply after reconnecting or re-registering tools." />
|
||||
<ui:VisualElement name="tool-category-container" class="tool-category-container" />
|
||||
</ui:VisualElement>
|
||||
</ui:VisualElement>
|
||||
</ui:UXML>
|
||||
@@ -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}
|
||||
Reference in New Issue
Block a user