Files
2026-07-13 12:49:17 +08:00

225 lines
8.0 KiB
C#

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";
private const string RemoteType = "remote";
private const string LocalType = "local";
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 or cannot be read.
/// Returns parsed JObject if valid JSON found.
/// Logs warning if file exists but contains malformed JSON.
/// </summary>
private JObject TryLoadConfig(string path)
{
if (!File.Exists(path))
return null;
string content;
try
{
content = File.ReadAllText(path);
}
catch (Exception ex)
{
UnityEngine.Debug.LogWarning($"[OpenCodeConfigurator] Failed to read config file {path}: {ex.Message}");
return null;
}
try
{
return JsonConvert.DeserializeObject<JObject>(content) ?? new JObject();
}
catch (JsonException ex)
{
// Malformed JSON - log warning and return null.
// When Configure() receives null, it will do: TryLoadConfig(path) ?? new JObject()
// This creates a fresh empty JObject, which replaces the entire file with only the unityMCP section.
// Existing config sections are lost. To preserve sections, a different recovery strategy
// (e.g., line-by-line parsing, JSON repair, or manual user intervention) would be needed.
UnityEngine.Debug.LogWarning($"[OpenCodeConfigurator] Malformed JSON in {path}: {ex.Message}");
return null;
}
}
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;
}
if (EntryMatchesCurrentTransport(unityMcp))
{
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);
// Load existing config or start fresh, preserving all other properties and MCP servers
var config = TryLoadConfig(path) ?? new JObject();
// Only add $schema if creating a new file
if (!File.Exists(path))
{
config["$schema"] = SchemaUrl;
}
// Preserve existing mcp section and only update our server entry
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()
{
if (HttpEndpointUtility.GetCurrentServerTransport() == ConfiguredTransport.Stdio)
{
var (uvxPath, _, packageName) = AssetPathUtility.GetUvxCommandParts();
if (string.IsNullOrWhiteSpace(uvxPath))
{
throw new InvalidOperationException("uvx not found. Install uv/uvx or set the override in Advanced Settings.");
}
var command = new JArray { uvxPath };
foreach (string value in AssetPathUtility.GetUvxDevFlagsList())
{
command.Add(value);
}
foreach (string value in AssetPathUtility.GetBetaServerFromArgsList())
{
command.Add(value);
}
command.Add(packageName);
command.Add("--transport");
command.Add("stdio");
return new JObject
{
["type"] = LocalType,
["command"] = command,
["enabled"] = true
};
}
return new JObject
{
["type"] = RemoteType,
["url"] = HttpEndpointUtility.GetMcpRpcUrl(),
["enabled"] = true
};
}
private bool EntryMatchesCurrentTransport(JObject entry)
{
string entryType = entry["type"]?.ToString();
ConfiguredTransport expected = HttpEndpointUtility.GetCurrentServerTransport();
if (expected == ConfiguredTransport.Stdio)
{
return string.Equals(entryType, LocalType, StringComparison.OrdinalIgnoreCase)
&& entry["command"] is JArray;
}
return string.Equals(entryType, RemoteType, StringComparison.OrdinalIgnoreCase)
&& UrlsEqual(entry["url"]?.ToString(), HttpEndpointUtility.GetMcpRpcUrl());
}
}
}