Skip to main content
Glama
McpClientConfigSection.cs16 kB
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.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 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; 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"); 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() { var clientNames = configurators.Select(c => c.DisplayName).ToList(); clientDropdown.choices = clientNames; if (clientNames.Count > 0) { clientDropdown.index = 0; } claudeCliPathRow.style.display = DisplayStyle.None; } 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]; 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 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() { if (selectedClientIndex >= 0 && selectedClientIndex < configurators.Count) { var client = configurators[selectedClientIndex]; RefreshClientStatus(client, forceImmediate: true); 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) { if (forceImmediate) { MCPServiceLocator.Client.CheckClientStatus(client, attemptAutoRewrite: false); lastStatusChecks[client] = DateTime.UtcNow; ApplyStatusToUi(client); return; } bool hasStatus = lastStatusChecks.ContainsKey(client); bool needsRefresh = !hasStatus || ShouldRefreshClient(client); if (!hasStatus) { ApplyStatusToUi(client, showChecking: true); } else { ApplyStatusToUi(client); } if (needsRefresh && !statusRefreshInFlight.Contains(client)) { statusRefreshInFlight.Add(client); ApplyStatusToUi(client, showChecking: true); Task.Run(() => { MCPServiceLocator.Client.CheckClientStatus(client, 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) { 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 = "Checking..."; clientStatusLabel.style.color = StyleKeyword.Null; clientStatusIndicator.AddToClassList("warning"); configureButton.text = client.GetConfigureActionLabel(); return; } clientStatusLabel.text = GetStatusDisplayString(client.Status); clientStatusLabel.style.color = StyleKeyword.Null; 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; } configureButton.text = client.GetConfigureActionLabel(); } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/CoplayDev/unity-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server