using System; using System.Collections.Generic; using System.IO; using MCPForUnity.Editor.Helpers; using MCPForUnity.Editor.Models; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace MCPForUnity.Editor.Clients.Configurators { /// /// Configurator for OpenCode (opencode.ai) - a Go-based terminal AI coding assistant. /// OpenCode uses ~/.config/opencode/opencode.json with a custom "mcp" format. /// public class OpenCodeConfigurator : McpClientConfiguratorBase { private const string ServerName = "unityMCP"; private const string SchemaUrl = "https://opencode.ai/config.json"; public OpenCodeConfigurator() : base(new McpClient { name = "OpenCode", windowsConfigPath = BuildConfigPath(), macConfigPath = BuildConfigPath(), linuxConfigPath = BuildConfigPath() }) { } private static string BuildConfigPath() { string xdgConfigHome = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME"); string configBase = !string.IsNullOrEmpty(xdgConfigHome) ? xdgConfigHome : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config"); return Path.Combine(configBase, "opencode", "opencode.json"); } public override string GetConfigPath() => CurrentOsPath(); /// /// Attempts to load and parse the config file. /// Returns null if file doesn't exist. /// Returns empty JObject if file exists but contains malformed JSON (logs warning). /// Throws on I/O errors (permission denied, etc.). /// private JObject TryLoadConfig(string path) { if (!File.Exists(path)) return null; string content = File.ReadAllText(path); try { return JsonConvert.DeserializeObject(content) ?? new JObject(); } catch (JsonException) { // Malformed JSON - return empty object so caller can overwrite with valid config UnityEngine.Debug.LogWarning($"[OpenCodeConfigurator] Malformed JSON in {path}, will overwrite with valid config"); return new JObject(); } } public override McpStatus CheckStatus(bool attemptAutoRewrite = true) { try { string path = GetConfigPath(); var config = TryLoadConfig(path); if (config == null) { client.SetStatus(McpStatus.NotConfigured); return client.status; } var unityMcp = config["mcp"]?[ServerName] as JObject; if (unityMcp == null) { client.SetStatus(McpStatus.NotConfigured); return client.status; } string configuredUrl = unityMcp["url"]?.ToString(); string expectedUrl = HttpEndpointUtility.GetMcpRpcUrl(); if (UrlsEqual(configuredUrl, expectedUrl)) { client.SetStatus(McpStatus.Configured); } else if (attemptAutoRewrite) { Configure(); } else { client.SetStatus(McpStatus.IncorrectPath); } } catch (Exception ex) { client.SetStatus(McpStatus.Error, ex.Message); } return client.status; } public override void Configure() { try { string path = GetConfigPath(); McpConfigurationHelper.EnsureConfigDirectoryExists(path); var config = TryLoadConfig(path) ?? new JObject { ["$schema"] = SchemaUrl }; var mcpSection = config["mcp"] as JObject ?? new JObject(); config["mcp"] = mcpSection; mcpSection[ServerName] = BuildServerEntry(); McpConfigurationHelper.WriteAtomicFile(path, JsonConvert.SerializeObject(config, Formatting.Indented)); client.SetStatus(McpStatus.Configured); } catch (Exception ex) { client.SetStatus(McpStatus.Error, ex.Message); } } public override string GetManualSnippet() { var snippet = new JObject { ["mcp"] = new JObject { [ServerName] = BuildServerEntry() } }; return JsonConvert.SerializeObject(snippet, Formatting.Indented); } public override IList GetInstallationSteps() => new List { "Install OpenCode (https://opencode.ai)", "Click Configure to add Unity MCP to ~/.config/opencode/opencode.json", "Restart OpenCode", "The Unity MCP server should be detected automatically" }; private static JObject BuildServerEntry() => new JObject { ["type"] = "remote", ["url"] = HttpEndpointUtility.GetMcpRpcUrl(), ["enabled"] = true }; } }