using System; using System.IO; using System.Security; using UnityEditor; using UnityEngine; namespace io.github.hatayama.uLoopMCP { /// /// Compile request data for session management. /// [Serializable] public class CompileRequestData { public string requestId; public string json; } /// /// Unity MCP Editor settings data. /// [Serializable] public record McpEditorSettingsData { public int customPort = McpServerConfig.DEFAULT_PORT; public string projectRootPath = ""; public string serverSessionId = ""; public bool showDeveloperTools = false; public bool enableMcpLogs = false; public bool enableCommunicationLogs = false; public bool enableDevelopmentMode = false; public string lastUsedConfigPath = ""; public string lastSeenSetupWizardVersion = ""; public bool suppressSetupWizardAutoShow = false; // UI State Settings public bool showSecuritySettings = true; public bool showToolSettings = true; // Repository Root Toggle public bool addRepositoryRoot = false; // Default to flat installation so first-time setup does not add an extra grouping layer unless requested. public bool installSkillsFlat = true; // Session State Settings (moved from McpSessionManager) // Default to true so the server starts automatically on fresh install public bool isServerRunning = true; public bool isAfterCompile = false; public bool isDomainReloadInProgress = false; public bool isReconnecting = false; public bool showReconnectingUI = false; public bool showPostCompileReconnectingUI = false; public int selectedEditorType = (int)McpEditorType.Cursor; public int connectionMode = (int)ConnectionMode.CLI; public float communicationLogHeight = McpUIConstants.DEFAULT_COMMUNICATION_LOG_HEIGHT; public string communicationLogsJson = "[]"; public string pendingRequestsJson = "{}"; public bool compileWindowHasData = false; public string[] pendingCompileRequestIds = new string[0]; public CompileRequestData[] compileRequests = new CompileRequestData[0]; public ConnectedLLMToolData[] connectedLLMTools = new ConnectedLLMToolData[0]; } /// /// Management class for Unity MCP Editor settings. /// Saves as a JSON file in the UserSettings folder. /// public static class McpEditorSettings { private static string SettingsFilePath => Path.Combine(McpConstants.USER_SETTINGS_FOLDER, McpConstants.SETTINGS_FILE_NAME); private static McpEditorSettingsData _cachedSettings; internal static void InvalidateCache() { _cachedSettings = null; } [InitializeOnLoadMethod] private static void RecoverSettingsFileOnEditorLoad() { if (AssetDatabase.IsAssetImportWorkerProcess()) { return; } RecoverSettingsFileIfNeeded(); } internal static void RecoverSettingsFileIfNeeded() { if (!IsValidSettingsPath(SettingsFilePath)) { throw new SecurityException($"Invalid settings file path: {SettingsFilePath}"); } AtomicFileWriter.RecoverSidecarFiles(SettingsFilePath); } /// /// Gets the settings data. /// public static McpEditorSettingsData GetSettings() { if (_cachedSettings == null) { LoadSettings(); } return _cachedSettings; } /// /// Saves the settings data. /// public static void SaveSettings(McpEditorSettingsData settings) { // Security: Validate settings file path if (!IsValidSettingsPath(SettingsFilePath)) { throw new SecurityException($"Invalid settings file path: {SettingsFilePath}"); } // Security: Ensure directory exists and create it safely string directory = Path.GetDirectoryName(SettingsFilePath); if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) { Directory.CreateDirectory(directory); } string json = JsonUtility.ToJson(settings, true); // Security: Validate JSON content size if (json.Length > McpConstants.MAX_SETTINGS_SIZE_BYTES) { throw new SecurityException("Settings JSON content exceeds size limit"); } AtomicFileWriter.Write(SettingsFilePath, json); _cachedSettings = settings; // Best-effort cleanup: even if this fails, .bak is overwritten on next save AtomicFileWriter.CleanupBackup(SettingsFilePath + ".bak"); } /// /// Applies a transformation to the current settings and saves once. /// Use when multiple fields need to be updated together to avoid redundant writes. /// public static void UpdateSettings(Func transform) { Debug.Assert(transform != null, "transform must not be null"); McpEditorSettingsData current = GetSettings(); McpEditorSettingsData updated = transform(current); SaveSettings(updated); } /// /// Gets the custom port number. /// public static int GetCustomPort() { return GetSettings().customPort; } public static string GetProjectRootPath() { return GetSettings().projectRootPath ?? string.Empty; } public static string GetServerSessionId() { return GetSettings().serverSessionId ?? string.Empty; } /// /// Saves the custom port number. /// public static void SetCustomPort(int port) { McpEditorSettingsData settings = GetSettings(); McpEditorSettingsData updatedSettings = settings with { customPort = port }; SaveSettings(updatedSettings); } public static void SetRunningServerSession(int port, string projectRootPath, string serverSessionId) { Debug.Assert(port > 0, "port must be positive"); Debug.Assert(!string.IsNullOrWhiteSpace(projectRootPath), "projectRootPath must not be empty"); Debug.Assert(!string.IsNullOrWhiteSpace(serverSessionId), "serverSessionId must not be empty"); string normalizedProjectRootPath = NormalizeProjectRootPath(projectRootPath); UpdateSettings(settings => settings with { isServerRunning = true, customPort = port, projectRootPath = normalizedProjectRootPath, serverSessionId = serverSessionId }); } /// /// Gets the Developer Tools display setting. /// public static bool GetShowDeveloperTools() { return GetSettings().showDeveloperTools; } /// /// Saves the Developer Tools display setting. /// public static void SetShowDeveloperTools(bool show) { McpEditorSettingsData settings = GetSettings(); McpEditorSettingsData updatedSettings = settings with { showDeveloperTools = show }; SaveSettings(updatedSettings); } /// /// Gets the MCP log enabled flag. /// public static bool GetEnableMcpLogs() { return GetSettings().enableMcpLogs; } /// /// Sets the MCP log enabled flag. /// public static void SetEnableMcpLogs(bool enableMcpLogs) { McpEditorSettingsData settings = GetSettings(); McpEditorSettingsData newSettings = settings with { enableMcpLogs = enableMcpLogs }; SaveSettings(newSettings); // Synchronize McpLogger settings as well. } /// /// Gets the communication logs enabled flag. /// public static bool GetEnableCommunicationLogs() { return GetSettings().enableCommunicationLogs; } public static string GetLastSeenSetupWizardVersion() { return GetSettings().lastSeenSetupWizardVersion ?? string.Empty; } public static void SetLastSeenSetupWizardVersion(string version) { string normalizedVersion = version ?? string.Empty; McpEditorSettingsData settings = GetSettings(); McpEditorSettingsData updatedSettings = settings with { lastSeenSetupWizardVersion = normalizedVersion }; SaveSettings(updatedSettings); } public static bool GetSuppressSetupWizardAutoShow() { return GetSettings().suppressSetupWizardAutoShow; } public static void SetSuppressSetupWizardAutoShow(bool suppressAutoShow) { McpEditorSettingsData settings = GetSettings(); McpEditorSettingsData updatedSettings = settings with { suppressSetupWizardAutoShow = suppressAutoShow }; SaveSettings(updatedSettings); } /// /// Sets the communication logs enabled flag. /// public static void SetEnableCommunicationLogs(bool enableCommunicationLogs) { McpEditorSettingsData settings = GetSettings(); McpEditorSettingsData newSettings = settings with { enableCommunicationLogs = enableCommunicationLogs }; SaveSettings(newSettings); } /// /// Gets the show security settings flag. /// public static bool GetShowSecuritySettings() { return GetSettings().showSecuritySettings; } /// /// Sets the show security settings flag. /// public static void SetShowSecuritySettings(bool showSecuritySettings) { McpEditorSettingsData settings = GetSettings(); McpEditorSettingsData newSettings = settings with { showSecuritySettings = showSecuritySettings }; SaveSettings(newSettings); } public static bool GetShowToolSettings() { return GetSettings().showToolSettings; } public static void SetShowToolSettings(bool showToolSettings) { McpEditorSettingsData settings = GetSettings(); McpEditorSettingsData newSettings = settings with { showToolSettings = showToolSettings }; SaveSettings(newSettings); } /// /// Gets the repository root usage flag. /// public static bool GetAddRepositoryRoot() { return GetSettings().addRepositoryRoot; } /// /// Sets the repository root usage flag. /// public static void SetAddRepositoryRoot(bool addRepositoryRoot) { McpEditorSettingsData settings = GetSettings(); McpEditorSettingsData newSettings = settings with { addRepositoryRoot = addRepositoryRoot }; SaveSettings(newSettings); } public static bool GetInstallSkillsFlat() { return GetSettings().installSkillsFlat; } public static void SetInstallSkillsFlat(bool installSkillsFlat) { McpEditorSettingsData settings = GetSettings(); McpEditorSettingsData newSettings = settings with { installSkillsFlat = installSkillsFlat }; SaveSettings(newSettings); } // Session State Settings Methods (moved from McpSessionManager) /// /// Gets the server running state. /// public static bool GetIsServerRunning() { return GetSettings().isServerRunning; } /// /// Sets the server running state. /// public static void SetIsServerRunning(bool isServerRunning) { McpEditorSettingsData settings = GetSettings(); McpEditorSettingsData newSettings = settings with { isServerRunning = isServerRunning }; SaveSettings(newSettings); } /// /// Gets the after compile flag. /// public static bool GetIsAfterCompile() { return GetSettings().isAfterCompile; } /// /// Sets the after compile flag. /// public static void SetIsAfterCompile(bool isAfterCompile) { McpEditorSettingsData settings = GetSettings(); McpEditorSettingsData newSettings = settings with { isAfterCompile = isAfterCompile }; SaveSettings(newSettings); } /// /// Gets the domain reload in progress flag. /// public static bool GetIsDomainReloadInProgress() { return GetSettings().isDomainReloadInProgress; } /// /// Sets the domain reload in progress flag. /// public static void SetIsDomainReloadInProgress(bool isDomainReloadInProgress) { McpEditorSettingsData settings = GetSettings(); McpEditorSettingsData newSettings = settings with { isDomainReloadInProgress = isDomainReloadInProgress }; SaveSettings(newSettings); } /// /// Gets the reconnecting flag. /// public static bool GetIsReconnecting() { return GetSettings().isReconnecting; } /// /// Sets the reconnecting flag. /// public static void SetIsReconnecting(bool isReconnecting) { McpEditorSettingsData settings = GetSettings(); McpEditorSettingsData newSettings = settings with { isReconnecting = isReconnecting }; SaveSettings(newSettings); } /// /// Gets the show reconnecting UI flag. /// public static bool GetShowReconnectingUI() { return GetSettings().showReconnectingUI; } /// /// Sets the show reconnecting UI flag. /// public static void SetShowReconnectingUI(bool showReconnectingUI) { McpEditorSettingsData settings = GetSettings(); McpEditorSettingsData newSettings = settings with { showReconnectingUI = showReconnectingUI }; SaveSettings(newSettings); } /// /// Gets the show post compile reconnecting UI flag. /// public static bool GetShowPostCompileReconnectingUI() { return GetSettings().showPostCompileReconnectingUI; } /// /// Sets the show post compile reconnecting UI flag. /// public static void SetShowPostCompileReconnectingUI(bool showPostCompileReconnectingUI) { McpEditorSettingsData settings = GetSettings(); McpEditorSettingsData newSettings = settings with { showPostCompileReconnectingUI = showPostCompileReconnectingUI }; SaveSettings(newSettings); } /// /// Gets the selected editor type. /// public static McpEditorType GetSelectedEditorType() { return (McpEditorType)GetSettings().selectedEditorType; } /// /// Sets the selected editor type. /// public static void SetSelectedEditorType(McpEditorType selectedEditorType) { McpEditorSettingsData settings = GetSettings(); McpEditorSettingsData newSettings = settings with { selectedEditorType = (int)selectedEditorType }; SaveSettings(newSettings); } public static ConnectionMode GetConnectionMode() { return (ConnectionMode)GetSettings().connectionMode; } public static void SetConnectionMode(ConnectionMode mode) { McpEditorSettingsData settings = GetSettings(); McpEditorSettingsData newSettings = settings with { connectionMode = (int)mode }; SaveSettings(newSettings); } /// /// Gets the communication log height. /// public static float GetCommunicationLogHeight() { return GetSettings().communicationLogHeight; } /// /// Sets the communication log height. /// public static void SetCommunicationLogHeight(float communicationLogHeight) { McpEditorSettingsData settings = GetSettings(); McpEditorSettingsData newSettings = settings with { communicationLogHeight = communicationLogHeight }; SaveSettings(newSettings); } /// /// Gets the communication logs JSON. /// public static string GetCommunicationLogsJson() { return GetSettings().communicationLogsJson; } /// /// Sets the communication logs JSON. /// public static void SetCommunicationLogsJson(string communicationLogsJson) { McpEditorSettingsData settings = GetSettings(); McpEditorSettingsData newSettings = settings with { communicationLogsJson = communicationLogsJson }; SaveSettings(newSettings); } /// /// Gets the pending requests JSON. /// public static string GetPendingRequestsJson() { return GetSettings().pendingRequestsJson; } /// /// Sets the pending requests JSON. /// public static void SetPendingRequestsJson(string pendingRequestsJson) { McpEditorSettingsData settings = GetSettings(); McpEditorSettingsData newSettings = settings with { pendingRequestsJson = pendingRequestsJson }; SaveSettings(newSettings); } /// /// Gets the compile window has data flag. /// public static bool GetCompileWindowHasData() { return GetSettings().compileWindowHasData; } /// /// Sets the compile window has data flag. /// public static void SetCompileWindowHasData(bool compileWindowHasData) { McpEditorSettingsData settings = GetSettings(); McpEditorSettingsData newSettings = settings with { compileWindowHasData = compileWindowHasData }; SaveSettings(newSettings); } // Helper methods that mirror McpSessionManager functionality /// /// Clear server session. /// public static void ClearServerSession() { UpdateSettings(settings => settings with { isServerRunning = false, serverSessionId = string.Empty }); } /// /// Clear after compile flag. /// public static void ClearAfterCompileFlag() { SetIsAfterCompile(false); } /// /// Clear reconnecting flags. /// public static void ClearReconnectingFlags() { UpdateSettings(s => s with { isReconnecting = false, showReconnectingUI = false }); } /// /// Clear post compile reconnecting UI. /// public static void ClearPostCompileReconnectingUI() { SetShowPostCompileReconnectingUI(false); } /// /// Clear domain reload flag. /// public static void ClearDomainReloadFlag() { SetIsDomainReloadInProgress(false); } /// /// Clear communication logs. /// public static void ClearCommunicationLogs() { UpdateSettings(s => s with { communicationLogsJson = "[]", pendingRequestsJson = "{}" }); } /// /// Clear compile window data. /// public static void ClearCompileWindowData() { SetCompileWindowHasData(false); } // Compile request management methods /// /// Gets the pending compile request IDs. /// public static string[] GetPendingCompileRequestIds() { return GetSettings().pendingCompileRequestIds; } /// /// Sets the pending compile request IDs. /// public static void SetPendingCompileRequestIds(string[] pendingCompileRequestIds) { McpEditorSettingsData settings = GetSettings(); McpEditorSettingsData newSettings = settings with { pendingCompileRequestIds = pendingCompileRequestIds }; SaveSettings(newSettings); } /// /// Gets the compile requests. /// public static CompileRequestData[] GetCompileRequests() { return GetSettings().compileRequests; } /// /// Sets the compile requests. /// public static void SetCompileRequests(CompileRequestData[] compileRequests) { McpEditorSettingsData settings = GetSettings(); McpEditorSettingsData newSettings = settings with { compileRequests = compileRequests }; SaveSettings(newSettings); } /// /// Gets the compile request JSON by request ID. /// public static string GetCompileRequestJson(string requestId) { CompileRequestData[] requests = GetCompileRequests(); CompileRequestData request = System.Array.Find(requests, r => r.requestId == requestId); return request?.json; } /// /// Sets the compile request JSON for a specific request ID. /// public static void SetCompileRequestJson(string requestId, string json) { CompileRequestData[] requests = GetCompileRequests(); CompileRequestData existingRequest = System.Array.Find(requests, r => r.requestId == requestId); if (existingRequest != null) { existingRequest.json = json; } else { CompileRequestData[] newRequests = new CompileRequestData[requests.Length + 1]; System.Array.Copy(requests, newRequests, requests.Length); newRequests[requests.Length] = new CompileRequestData { requestId = requestId, json = json }; requests = newRequests; } SetCompileRequests(requests); } /// /// Clears all compile requests. /// public static void ClearAllCompileRequests() { SetCompileRequests(new CompileRequestData[0]); SetPendingCompileRequestIds(new string[0]); } /// /// Adds a pending compile request. /// public static void AddPendingCompileRequest(string requestId) { string[] pendingIds = GetPendingCompileRequestIds(); if (System.Array.IndexOf(pendingIds, requestId) == -1) { string[] newPendingIds = new string[pendingIds.Length + 1]; System.Array.Copy(pendingIds, newPendingIds, pendingIds.Length); newPendingIds[pendingIds.Length] = requestId; SetPendingCompileRequestIds(newPendingIds); } } /// /// Removes a pending compile request. /// public static void RemovePendingCompileRequest(string requestId) { string[] pendingIds = GetPendingCompileRequestIds(); int index = System.Array.IndexOf(pendingIds, requestId); if (index != -1) { string[] newPendingIds = new string[pendingIds.Length - 1]; System.Array.Copy(pendingIds, 0, newPendingIds, 0, index); System.Array.Copy(pendingIds, index + 1, newPendingIds, index, pendingIds.Length - index - 1); SetPendingCompileRequestIds(newPendingIds); } } // Connected LLM Tools management methods /// /// Gets the connected LLM tools. /// public static ConnectedLLMToolData[] GetConnectedLLMTools() { return GetSettings().connectedLLMTools; } /// /// Sets the connected LLM tools. /// public static void SetConnectedLLMTools(ConnectedLLMToolData[] connectedLLMTools) { McpEditorSettingsData settings = GetSettings(); McpEditorSettingsData newSettings = settings with { connectedLLMTools = connectedLLMTools }; SaveSettings(newSettings); } /// /// Adds a connected LLM tool. /// public static void AddConnectedLLMTool(ConnectedLLMToolData toolData) { if (toolData == null || string.IsNullOrEmpty(toolData.Name)) { return; } ConnectedLLMToolData[] tools = GetConnectedLLMTools(); // Remove existing tool with same name if present ConnectedLLMToolData[] filteredTools = System.Array.FindAll(tools, t => t.Name != toolData.Name); // Add new tool ConnectedLLMToolData[] newTools = new ConnectedLLMToolData[filteredTools.Length + 1]; System.Array.Copy(filteredTools, newTools, filteredTools.Length); newTools[filteredTools.Length] = toolData; SetConnectedLLMTools(newTools); } /// /// Removes a connected LLM tool by name. /// public static void RemoveConnectedLLMTool(string toolName) { if (string.IsNullOrEmpty(toolName)) { return; } ConnectedLLMToolData[] tools = GetConnectedLLMTools(); ConnectedLLMToolData[] newTools = System.Array.FindAll(tools, t => t.Name != toolName); if (newTools.Length != tools.Length) { SetConnectedLLMTools(newTools); } } /// /// Clears all connected LLM tools. /// public static void ClearConnectedLLMTools() { SetConnectedLLMTools(new ConnectedLLMToolData[0]); } /// /// Loads the settings file. /// private static void LoadSettings() { try { // Security: Validate settings file path if (!IsValidSettingsPath(SettingsFilePath)) { throw new SecurityException($"Invalid settings file path: {SettingsFilePath}"); } RecoverSettingsFileIfNeeded(); if (File.Exists(SettingsFilePath)) { // Security: Check file size before reading FileInfo fileInfo = new FileInfo(SettingsFilePath); if (fileInfo.Length > McpConstants.MAX_SETTINGS_SIZE_BYTES) { throw new SecurityException("Settings file exceeds size limit"); } string json = File.ReadAllText(SettingsFilePath); // Security: Validate JSON content if (string.IsNullOrWhiteSpace(json)) { throw new InvalidDataException("Settings file contains invalid JSON content"); } _cachedSettings = JsonUtility.FromJson(json); // Migrate security fields before any potential SaveSettings call from this class. // If SaveSettings runs first, legacy security fields are stripped from JSON // because McpEditorSettingsData no longer defines them. ULoopSettings.GetSettings(); } else { _cachedSettings = new McpEditorSettingsData(); } } catch (Exception ex) { // Don't suppress this exception - corrupted settings should be reported throw new InvalidOperationException( $"Failed to load MCP Editor settings from: {SettingsFilePath}. Settings file may be corrupted.", ex); } } private static string NormalizeProjectRootPath(string projectRootPath) { string fullPath = Path.GetFullPath(projectRootPath); return fullPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); } /// /// Security: Validate if the settings file path is safe /// private static bool IsValidSettingsPath(string path) { try { // Normalize the path to prevent path traversal string normalizedPath = Path.GetFullPath(path); // Must be under UserSettings directory string expectedUserSettingsPath = Path.GetFullPath(McpConstants.USER_SETTINGS_FOLDER); // Check if path is within the expected directory return normalizedPath.StartsWith(expectedUserSettingsPath, StringComparison.OrdinalIgnoreCase) && normalizedPath.EndsWith(McpConstants.SETTINGS_FILE_NAME, StringComparison.OrdinalIgnoreCase); } catch (Exception ex) { UnityEngine.Debug.LogError($"{McpConstants.SECURITY_LOG_PREFIX} Error validating settings path {path}: {ex.Message}"); return false; } } } }