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
parent
9682e3c3e1
commit
30d5bc254e
|
|
@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 489f99ffb7e6743e88e3203552c8b37b
|
||||||
Loading…
Reference in New Issue