using System.Collections.Generic; using System.Linq; namespace io.github.hatayama.uLoopMCP { /// /// A class responsible for the business logic of MCP settings. /// Single Responsibility Principle: Responsible only for the business logic of settings management. /// /// Design document reference: Packages/src/Editor/ARCHITECTURE.md /// /// Related classes: /// - McpConfigRepository: Handles file I/O operations for configuration persistence /// - McpServerConfigFactory: Creates server configuration objects with proper formatting /// - McpConfigServiceFactory: Factory that creates and manages McpConfigService instances /// - UnityMcpPathResolver: Provides configuration file paths for different editors /// - McpServerConfigComparer: Compares server configurations to detect changes /// - McpPortValidator: Validates port numbers for configuration /// - McpEditorSettings: Manages editor-specific settings like custom port /// public class McpConfigService : IMcpConfigService { private readonly McpConfigRepository _repository; private readonly McpEditorType _editorType; public McpConfigService(McpConfigRepository repository, McpEditorType editorType) { _repository = repository; _editorType = editorType; } /// /// Checks if the editor settings exist. /// public bool IsConfigured() { string configPath = UnityMcpPathResolver.GetConfigPath(_editorType); if (!_repository.Exists(configPath)) { return false; } McpConfig config = _repository.Load(configPath); // Check if a setting with a port number exists. return config.mcpServers.Keys.Any(key => key.StartsWith(McpConstants.PROJECT_NAME)); } /// /// Checks if update is needed for the editor settings. /// /// The port number to check. /// True if update is needed, false if settings are already up to date. public bool IsUpdateNeeded(int port) { string configPath = UnityMcpPathResolver.GetConfigPath(_editorType); // If configuration file doesn't exist, update is needed if (!_repository.Exists(configPath)) { return true; } // Load existing settings McpConfig config = _repository.Load(configPath); // Check if Unity MCP configuration exists string serverKey = McpServerConfigFactory.CreateUnityMcpServerKey(port, _editorType); if (!config.mcpServers.ContainsKey(serverKey)) { return true; } // Create expected configuration string serverPath = UnityMcpPathResolver.GetTypeScriptServerPath(); McpServerConfigData expectedConfig = McpServerConfigFactory.CreateUnityMcpConfig(port, serverPath, _editorType); // Compare with existing configuration McpServerConfigData existingConfig = config.mcpServers[serverKey]; bool configsAreEqual = McpServerConfigComparer.AreEqual(existingConfig, expectedConfig); return !configsAreEqual; } /// /// Auto-configures the editor settings. /// /// The port number to use. public void AutoConfigure(int port) { if (!NodePathResolver.IsNodeAvailable()) { UnityEditor.EditorUtility.DisplayDialog( "Node.js Not Found", "Node.js is not installed or PATH is not configured.\n\n" + "To verify, run 'node -v' in your terminal.\n\n" + "You can download Node.js from: https://nodejs.org/", "OK" ); return; } string configPath = UnityMcpPathResolver.GetConfigPath(_editorType); // Create the settings directory (only if necessary). _repository.CreateConfigDirectory(configPath); // Load existing settings (or create new ones if they don't exist). McpConfig config = _repository.Load(configPath); // For Windsurf, keep the original behavior (always create new key) // For other editors, remove existing uLoopMCP configuration and create new one with updated port string serverKey = McpServerConfigFactory.CreateUnityMcpServerKey(port, _editorType); bool shouldReplaceExistingKey = _editorType != McpEditorType.Windsurf; Dictionary updatedServers = new(config.mcpServers); if (shouldReplaceExistingKey) { // Try to find existing uLoopMCP configuration string existingKey = FindExistingULoopMCPConfigurationKey(config); if (!string.IsNullOrEmpty(existingKey) && existingKey != serverKey) { // Remove existing configuration with different key updatedServers.Remove(existingKey); } } // Create new settings. string serverPath = UnityMcpPathResolver.GetTypeScriptServerPath(); McpServerConfigData newConfig = McpServerConfigFactory.CreateUnityMcpConfig(port, serverPath, _editorType); // Compare with existing settings and update if there are differences. bool needsUpdate = true; if (updatedServers.ContainsKey(serverKey)) { McpServerConfigData existingConfig = updatedServers[serverKey]; needsUpdate = !McpServerConfigComparer.AreEqual(existingConfig, newConfig); if (needsUpdate) { } } if (needsUpdate) { // Add/update Unity MCP settings. updatedServers[serverKey] = newConfig; McpConfig updatedConfig = new(updatedServers); _repository.Save(configPath, updatedConfig); string editorName = GetEditorDisplayName(_editorType); } } public void DeleteConfiguration() { string configPath = UnityMcpPathResolver.GetConfigPath(_editorType); _repository.DeleteULoopMCPEntries(configPath); } /// /// Finds existing Unity MCP configuration key in the loaded config. /// Returns the first found uLoopMCP configuration key, or null if none exists. /// /// The loaded MCP configuration /// Existing uLoopMCP configuration key, or null if not found private string FindExistingULoopMCPConfigurationKey(McpConfig config) { foreach (System.Collections.Generic.KeyValuePair serverEntry in config.mcpServers) { if (!serverEntry.Key.StartsWith(McpConstants.PROJECT_NAME)) continue; if (!serverEntry.Value.env.ContainsKey(McpConstants.UNITY_TCP_PORT_ENV_KEY)) continue; // Found existing uLoopMCP configuration return serverEntry.Key; } return null; } /// /// Updates environment variables for development mode and MCP debug logs. /// /// The port number to use. /// Whether to enable development mode. /// Whether to enable MCP debug logs. public void UpdateDevelopmentSettings(int port, bool developmentMode, bool enableMcpLogs) { string configPath = UnityMcpPathResolver.GetConfigPath(_editorType); // Create the settings directory (only if necessary) _repository.CreateConfigDirectory(configPath); // Load existing settings (or create new ones if they don't exist) McpConfig config = _repository.Load(configPath); // For Windsurf, keep the original behavior (always use port-based key) // For other editors, remove existing uLoopMCP configuration and create new one with updated port string serverKey = McpServerConfigFactory.CreateUnityMcpServerKey(port, _editorType); bool shouldReplaceExistingKey = _editorType != McpEditorType.Windsurf; Dictionary updatedServers = new(config.mcpServers); if (shouldReplaceExistingKey) { // Try to find existing uLoopMCP configuration string existingKey = FindExistingULoopMCPConfigurationKey(config); if (!string.IsNullOrEmpty(existingKey) && existingKey != serverKey) { // Remove existing configuration with different key updatedServers.Remove(existingKey); } } // Check if our server configuration exists if (!updatedServers.ContainsKey(serverKey)) { // If it doesn't exist, create it with full configuration AutoConfigure(port); // Then update the development settings UpdateDevelopmentSettingsOnly(port, developmentMode, enableMcpLogs); return; } // Update only the development settings UpdateDevelopmentSettingsOnly(port, developmentMode, enableMcpLogs); } /// /// Updates only the environment variables for development mode and MCP debug logs. /// /// The port number. /// Whether to enable development mode. /// Whether to enable MCP debug logs. private void UpdateDevelopmentSettingsOnly(int port, bool developmentMode, bool enableMcpLogs) { string configPath = UnityMcpPathResolver.GetConfigPath(_editorType); // Load existing settings McpConfig config = _repository.Load(configPath); // For Windsurf, keep the original behavior (always use port-based key) // For other editors, remove existing uLoopMCP configuration and create new one with updated port string serverKey = McpServerConfigFactory.CreateUnityMcpServerKey(port, _editorType); bool shouldReplaceExistingKey = _editorType != McpEditorType.Windsurf; Dictionary updatedServers = new(config.mcpServers); if (shouldReplaceExistingKey) { // Try to find existing uLoopMCP configuration string existingKey = FindExistingULoopMCPConfigurationKey(config); if (!string.IsNullOrEmpty(existingKey) && existingKey != serverKey) { // Remove existing configuration with different key updatedServers.Remove(existingKey); } } if (!updatedServers.ContainsKey(serverKey)) { return; } // Get existing configuration McpServerConfigData existingConfig = updatedServers[serverKey]; // Create new environment variables based on existing ones Dictionary updatedEnv = new(existingConfig.env); // Update the port number in environment variables updatedEnv[McpConstants.UNITY_TCP_PORT_ENV_KEY] = port.ToString(); // Remove old development mode environment variables (cleanup legacy settings) updatedEnv.Remove(McpConstants.ENV_KEY_ULOOPMCP_DEBUG); updatedEnv.Remove(McpConstants.ENV_KEY_ULOOPMCP_PRODUCTION); updatedEnv.Remove(McpConstants.ENV_KEY_MCP_DEBUG); updatedEnv.Remove(McpConstants.ENV_KEY_NODE_OPTIONS); // MCP_CLIENT_NAME is no longer used - client identification handled by MCP protocol // Add MCP_DEBUG for MCP debug logs if (enableMcpLogs) { updatedEnv[McpConstants.ENV_KEY_MCP_DEBUG] = McpConstants.ENV_VALUE_TRUE; // Enable source maps when debug mode is enabled for better stack traces updatedEnv[McpConstants.ENV_KEY_NODE_OPTIONS] = McpConstants.NODE_OPTIONS_ENABLE_SOURCE_MAPS; } // For disabled logs, simply don't set MCP_DEBUG and NODE_OPTIONS (default behavior) // Create updated configuration (keeping command and args unchanged) McpServerConfigData updatedConfig = new McpServerConfigData( existingConfig.command, existingConfig.args, updatedEnv ); // Update only this server's configuration updatedServers[serverKey] = updatedConfig; // Save the updated configuration McpConfig updatedMcpConfig = new(updatedServers); _repository.Save(configPath, updatedMcpConfig); string editorName = GetEditorDisplayName(_editorType); // Log environment variables for debugging } /// /// Gets the display name of the editor. /// private string GetEditorDisplayName(McpEditorType editorType) { return editorType switch { McpEditorType.Cursor => "Cursor", McpEditorType.ClaudeCode => "Claude Code", _ => editorType.ToString() }; } /// /// Gets the configured port number from the editor settings. /// If server is running, tries to find config matching the current server port. /// Otherwise returns the first available configuration. /// /// The configured port number. public int GetConfiguredPort() { McpConfig config = LoadConfiguration(); System.Collections.Generic.List<(string key, int port)> uloopmcpConfigs = ExtractULoopMCPConfigurations(config); return SelectBestMatchingPort(uloopmcpConfigs); } /// /// Load configuration file from the appropriate path /// /// The loaded MCP configuration private McpConfig LoadConfiguration() { string configPath = UnityMcpPathResolver.GetConfigPath(_editorType); if (!_repository.Exists(configPath)) { throw new System.InvalidOperationException("Configuration file not found."); } return _repository.Load(configPath); } /// /// Extract Unity MCP configurations from the loaded config /// /// The loaded MCP configuration /// List of Unity MCP server configurations with their ports private System.Collections.Generic.List<(string key, int port)> ExtractULoopMCPConfigurations(McpConfig config) { System.Collections.Generic.List<(string key, int port)> uloopmcpConfigs = new System.Collections.Generic.List<(string key, int port)>(); foreach (System.Collections.Generic.KeyValuePair serverEntry in config.mcpServers) { if (!serverEntry.Key.StartsWith(McpConstants.PROJECT_NAME)) continue; if (!serverEntry.Value.env.ContainsKey(McpConstants.UNITY_TCP_PORT_ENV_KEY)) continue; string portString = serverEntry.Value.env[McpConstants.UNITY_TCP_PORT_ENV_KEY]; if (int.TryParse(portString, out int port)) { uloopmcpConfigs.Add((serverEntry.Key, port)); } } if (uloopmcpConfigs.Count == 0) { throw new System.InvalidOperationException("Unity MCP server configuration not found."); } return uloopmcpConfigs; } /// /// Select the best matching port based on server status /// /// Available Unity MCP configurations /// The most appropriate port number private int SelectBestMatchingPort(System.Collections.Generic.List<(string key, int port)> uloopmcpConfigs) { bool serverIsRunning = McpServerController.IsServerRunning; if (!serverIsRunning) return uloopmcpConfigs[0].port; int currentServerPort = McpServerController.ServerPort; (string key, int port) matchingConfig = uloopmcpConfigs.FirstOrDefault(c => c.port == currentServerPort); return matchingConfig != default ? matchingConfig.port : uloopmcpConfigs[0].port; } /// /// Validates configuration parameters for fail-fast behavior /// private void ValidateConfigurationParameters(int port) { // Validate port number using shared validator if (!McpPortValidator.ValidatePort(port, $"for {_editorType} configuration")) { throw new System.ArgumentOutOfRangeException(nameof(port), $"Invalid port number for {_editorType} configuration: {port}. Port must be between 1 and 65535."); } // Validate editor type if (!System.Enum.IsDefined(typeof(McpEditorType), _editorType)) { throw new System.InvalidOperationException( $"Cannot configure settings for invalid editor type: {_editorType}"); } } } }