feat: Add OpenCode (opencode.ai) client configurator (#608)

* 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 <akshay@kiddopia.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: akshay-kiddopia <akshay@kiddopia.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
main
dsarno 2026-01-21 19:46:19 -08:00 committed by GitHub
parent 9682e3c3e1
commit 30d5bc254e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 158 additions and 0 deletions

View File

@ -0,0 +1,156 @@
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
{
/// <summary>
/// Configurator for OpenCode (opencode.ai) - a Go-based terminal AI coding assistant.
/// OpenCode uses ~/.config/opencode/opencode.json with a custom "mcp" format.
/// </summary>
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();
/// <summary>
/// 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.).
/// </summary>
private JObject TryLoadConfig(string path)
{
if (!File.Exists(path))
return null;
string content = File.ReadAllText(path);
try
{
return JsonConvert.DeserializeObject<JObject>(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<string> GetInstallationSteps() => new List<string>
{
"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
};
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 489f99ffb7e6743e88e3203552c8b37b