422 lines
18 KiB
C#
422 lines
18 KiB
C#
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
namespace io.github.hatayama.uLoopMCP
|
|
{
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
public class McpConfigService : IMcpConfigService
|
|
{
|
|
private readonly McpConfigRepository _repository;
|
|
private readonly McpEditorType _editorType;
|
|
|
|
public McpConfigService(McpConfigRepository repository, McpEditorType editorType)
|
|
{
|
|
_repository = repository;
|
|
_editorType = editorType;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if the editor settings exist.
|
|
/// </summary>
|
|
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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if update is needed for the editor settings.
|
|
/// </summary>
|
|
/// <param name="port">The port number to check.</param>
|
|
/// <returns>True if update is needed, false if settings are already up to date.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Auto-configures the editor settings.
|
|
/// </summary>
|
|
/// <param name="port">The port number to use.</param>
|
|
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<string, McpServerConfigData> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds existing Unity MCP configuration key in the loaded config.
|
|
/// Returns the first found uLoopMCP configuration key, or null if none exists.
|
|
/// </summary>
|
|
/// <param name="config">The loaded MCP configuration</param>
|
|
/// <returns>Existing uLoopMCP configuration key, or null if not found</returns>
|
|
private string FindExistingULoopMCPConfigurationKey(McpConfig config)
|
|
{
|
|
foreach (System.Collections.Generic.KeyValuePair<string, McpServerConfigData> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates environment variables for development mode and MCP debug logs.
|
|
/// </summary>
|
|
/// <param name="port">The port number to use.</param>
|
|
/// <param name="developmentMode">Whether to enable development mode.</param>
|
|
/// <param name="enableMcpLogs">Whether to enable MCP debug logs.</param>
|
|
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<string, McpServerConfigData> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates only the environment variables for development mode and MCP debug logs.
|
|
/// </summary>
|
|
/// <param name="port">The port number.</param>
|
|
/// <param name="developmentMode">Whether to enable development mode.</param>
|
|
/// <param name="enableMcpLogs">Whether to enable MCP debug logs.</param>
|
|
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<string, McpServerConfigData> 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<string, string> 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
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the display name of the editor.
|
|
/// </summary>
|
|
private string GetEditorDisplayName(McpEditorType editorType)
|
|
{
|
|
return editorType switch
|
|
{
|
|
McpEditorType.Cursor => "Cursor",
|
|
McpEditorType.ClaudeCode => "Claude Code",
|
|
_ => editorType.ToString()
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <returns>The configured port number.</returns>
|
|
public int GetConfiguredPort()
|
|
{
|
|
McpConfig config = LoadConfiguration();
|
|
System.Collections.Generic.List<(string key, int port)> uloopmcpConfigs = ExtractULoopMCPConfigurations(config);
|
|
return SelectBestMatchingPort(uloopmcpConfigs);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Load configuration file from the appropriate path
|
|
/// </summary>
|
|
/// <returns>The loaded MCP configuration</returns>
|
|
private McpConfig LoadConfiguration()
|
|
{
|
|
string configPath = UnityMcpPathResolver.GetConfigPath(_editorType);
|
|
|
|
if (!_repository.Exists(configPath))
|
|
{
|
|
throw new System.InvalidOperationException("Configuration file not found.");
|
|
}
|
|
|
|
return _repository.Load(configPath);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Extract Unity MCP configurations from the loaded config
|
|
/// </summary>
|
|
/// <param name="config">The loaded MCP configuration</param>
|
|
/// <returns>List of Unity MCP server configurations with their ports</returns>
|
|
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<string, McpServerConfigData> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Select the best matching port based on server status
|
|
/// </summary>
|
|
/// <param name="uloopmcpConfigs">Available Unity MCP configurations</param>
|
|
/// <returns>The most appropriate port number</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates configuration parameters for fail-fast behavior
|
|
/// </summary>
|
|
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}");
|
|
}
|
|
}
|
|
}
|
|
} |