From 7f0bf045643237f90b3d4aa6e657b6f78e311f74 Mon Sep 17 00:00:00 2001 From: David Sarno Date: Wed, 21 Jan 2026 19:29:48 -0800 Subject: [PATCH 1/2] feat: Add OpenCode (opencode.ai) client configurator Add support for the OpenCode CLI client with automatic configuration. - Create OpenCodeConfigurator implementing IClientConfigurator - Configure via ~/.config/opencode/opencode.json (XDG standard path) - Use McpConfigurationHelper for atomic file writes and directory creation - Support both new config creation and merging with existing config Co-Authored-By: akshay-kiddopia Co-Authored-By: Claude Opus 4.5 --- .../Configurators/OpenCodeConfigurator.cs | 123 ++++++++++++++++++ .../OpenCodeConfigurator.cs.meta | 2 + 2 files changed, 125 insertions(+) create mode 100644 MCPForUnity/Editor/Clients/Configurators/OpenCodeConfigurator.cs create mode 100644 MCPForUnity/Editor/Clients/Configurators/OpenCodeConfigurator.cs.meta diff --git a/MCPForUnity/Editor/Clients/Configurators/OpenCodeConfigurator.cs b/MCPForUnity/Editor/Clients/Configurators/OpenCodeConfigurator.cs new file mode 100644 index 000000000..c72e47b6b --- /dev/null +++ b/MCPForUnity/Editor/Clients/Configurators/OpenCodeConfigurator.cs @@ -0,0 +1,123 @@ +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 home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + return Path.Combine(home, ".config", "opencode", "opencode.json"); + } + + public override string GetConfigPath() => CurrentOsPath(); + + public override McpStatus CheckStatus(bool attemptAutoRewrite = true) + { + try + { + string path = GetConfigPath(); + if (!File.Exists(path)) + { + client.SetStatus(McpStatus.NotConfigured); + return client.status; + } + + var config = JsonConvert.DeserializeObject(File.ReadAllText(path)); + 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() + { + string path = GetConfigPath(); + McpConfigurationHelper.EnsureConfigDirectoryExists(path); + + JObject config = File.Exists(path) + ? JsonConvert.DeserializeObject(File.ReadAllText(path)) ?? new JObject() + : 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); + } + + 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 + }; + } +} diff --git a/MCPForUnity/Editor/Clients/Configurators/OpenCodeConfigurator.cs.meta b/MCPForUnity/Editor/Clients/Configurators/OpenCodeConfigurator.cs.meta new file mode 100644 index 000000000..8094fb1d0 --- /dev/null +++ b/MCPForUnity/Editor/Clients/Configurators/OpenCodeConfigurator.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 489f99ffb7e6743e88e3203552c8b37b \ No newline at end of file From c71a0eefe1eb382d3d3178a6efc9214b50a00c10 Mon Sep 17 00:00:00 2001 From: David Sarno Date: Wed, 21 Jan 2026 19:45:07 -0800 Subject: [PATCH 2/2] fix: Address code review feedback for OpenCodeConfigurator - Add TryLoadConfig() helper to consolidate file read/parse logic - Handle JsonException separately (log warning, return empty object to overwrite) - Wrap Configure() in try/catch to prevent crashes, set McpStatus.Error on failure - Respect XDG_CONFIG_HOME environment variable per XDG Base Directory spec Co-Authored-By: Claude Opus 4.5 --- .../Configurators/OpenCodeConfigurator.cs | 63 ++++++++++++++----- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/MCPForUnity/Editor/Clients/Configurators/OpenCodeConfigurator.cs b/MCPForUnity/Editor/Clients/Configurators/OpenCodeConfigurator.cs index c72e47b6b..a0ec94117 100644 --- a/MCPForUnity/Editor/Clients/Configurators/OpenCodeConfigurator.cs +++ b/MCPForUnity/Editor/Clients/Configurators/OpenCodeConfigurator.cs @@ -28,25 +28,53 @@ public OpenCodeConfigurator() : base(new McpClient private static string BuildConfigPath() { - string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - return Path.Combine(home, ".config", "opencode", "opencode.json"); + 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(); - if (!File.Exists(path)) + var config = TryLoadConfig(path); + + if (config == null) { client.SetStatus(McpStatus.NotConfigured); return client.status; } - var config = JsonConvert.DeserializeObject(File.ReadAllText(path)); - var unityMcp = config?["mcp"]?[ServerName] as JObject; + var unityMcp = config["mcp"]?[ServerName] as JObject; if (unityMcp == null) { @@ -80,20 +108,25 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true) public override void Configure() { - string path = GetConfigPath(); - McpConfigurationHelper.EnsureConfigDirectoryExists(path); + try + { + string path = GetConfigPath(); + McpConfigurationHelper.EnsureConfigDirectoryExists(path); - JObject config = File.Exists(path) - ? JsonConvert.DeserializeObject(File.ReadAllText(path)) ?? new JObject() - : new JObject { ["$schema"] = SchemaUrl }; + var config = TryLoadConfig(path) ?? new JObject { ["$schema"] = SchemaUrl }; - var mcpSection = config["mcp"] as JObject ?? new JObject(); - config["mcp"] = mcpSection; + var mcpSection = config["mcp"] as JObject ?? new JObject(); + config["mcp"] = mcpSection; - mcpSection[ServerName] = BuildServerEntry(); + mcpSection[ServerName] = BuildServerEntry(); - McpConfigurationHelper.WriteAtomicFile(path, JsonConvert.SerializeObject(config, Formatting.Indented)); - client.SetStatus(McpStatus.Configured); + 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()