Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
284 changes: 185 additions & 99 deletions MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -436,151 +436,160 @@ public abstract class CodexMcpConfigurator : McpClientConfiguratorBase
{
public CodexMcpConfigurator(McpClient client) : base(client) { }

public override string GetConfigPath() => CurrentOsPath();

public override bool IsInstalled => ParentDirectoryExists(GetConfigPath());
public override string GetConfigPath() => GetPreferredWriteConfigPath();

public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
public override bool IsInstalled
{
try
get
{
string path = GetConfigPath();
if (!File.Exists(path))
foreach (var path in GetCandidateConfigPaths())
{
client.SetStatus(McpStatus.NotConfigured);
client.configuredTransport = Models.ConfiguredTransport.Unknown;
return client.status;
if (ParentDirectoryExists(path)) return true;
}
return false;
}
}

string toml = File.ReadAllText(path);
if (CodexConfigHelper.TryParseCodexServer(toml, out _, out var args, out var url))
public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
{
try
{
bool sawConfigFile = false;
foreach (string path in GetCandidateConfigPaths())
{
// Determine and set the configured transport type
if (!string.IsNullOrEmpty(url))
if (string.IsNullOrEmpty(path) || !File.Exists(path))
{
// Distinguish HTTP Local from HTTP Remote
string remoteRpcUrl = HttpEndpointUtility.GetRemoteMcpRpcUrl();
if (!string.IsNullOrEmpty(remoteRpcUrl) && UrlsEqual(url, remoteRpcUrl))
{
client.configuredTransport = Models.ConfiguredTransport.HttpRemote;
}
else
{
client.configuredTransport = Models.ConfiguredTransport.Http;
}
}
else if (args != null && args.Length > 0)
{
client.configuredTransport = Models.ConfiguredTransport.Stdio;
continue;
}
else

sawConfigFile = true;
string toml = File.ReadAllText(path);
if (!CodexConfigHelper.TryParseCodexServer(toml, out _, out var args, out var url))
{
client.configuredTransport = Models.ConfiguredTransport.Unknown;
continue;
}

bool matches = false;
bool hasVersionMismatch = false;
string mismatchReason = null;
return CheckParsedCodexServer(path, args, url, attemptAutoRewrite);
}

if (!string.IsNullOrEmpty(url))
{
// Match against the active scope's URL
matches = UrlsEqual(url, HttpEndpointUtility.GetMcpRpcUrl());
}
else if (args != null && args.Length > 0)
{
// Use beta-aware expected package source for comparison
string expected = GetExpectedPackageSourceForValidation();
string configured = McpConfigurationHelper.ExtractUvxUrl(args);
client.SetStatus(sawConfigFile ? McpStatus.MissingConfig : McpStatus.NotConfigured);
client.configuredTransport = Models.ConfiguredTransport.Unknown;
}
catch (Exception ex)
{
client.SetStatus(McpStatus.Error, ex.Message);
client.configuredTransport = Models.ConfiguredTransport.Unknown;
}

if (!string.IsNullOrEmpty(configured) && !string.IsNullOrEmpty(expected))
{
if (McpConfigurationHelper.PathsEqual(configured, expected))
{
matches = true;
}
else
{
// Check for beta/stable mismatch
bool configuredIsBeta = IsBetaPackageSource(configured);
bool expectedIsBeta = IsBetaPackageSource(expected);
return client.status;
}

if (configuredIsBeta && !expectedIsBeta)
{
hasVersionMismatch = true;
mismatchReason = "Configured for prerelease server, but this package is stable. Re-configure to switch to stable.";
}
else if (!configuredIsBeta && expectedIsBeta)
{
hasVersionMismatch = true;
mismatchReason = "Configured for stable server, but this package is prerelease. Re-configure to switch to prerelease.";
}
else
{
hasVersionMismatch = true;
mismatchReason = "Server version doesn't match the plugin. Re-configure to update.";
}
}
}
}
private McpStatus CheckParsedCodexServer(string path, string[] args, string url, bool attemptAutoRewrite)
{
if (!string.IsNullOrEmpty(url))
{
string remoteRpcUrl = HttpEndpointUtility.GetRemoteMcpRpcUrl();
client.configuredTransport =
!string.IsNullOrEmpty(remoteRpcUrl) && UrlsEqual(url, remoteRpcUrl)
? Models.ConfiguredTransport.HttpRemote
: Models.ConfiguredTransport.Http;
}
else if (args != null && args.Length > 0)
{
client.configuredTransport = Models.ConfiguredTransport.Stdio;
}
else
{
client.configuredTransport = Models.ConfiguredTransport.Unknown;
}

bool matches = false;
bool hasVersionMismatch = false;
string mismatchReason = null;

if (!string.IsNullOrEmpty(url))
{
matches = UrlsEqual(url, HttpEndpointUtility.GetMcpRpcUrl());
}
else if (args != null && args.Length > 0)
{
string expected = GetExpectedPackageSourceForValidation();
string configured = McpConfigurationHelper.ExtractUvxUrl(args);

if (matches)
if (!string.IsNullOrEmpty(configured) && !string.IsNullOrEmpty(expected))
{
if (McpConfigurationHelper.PathsEqual(configured, expected))
{
client.SetStatus(McpStatus.Configured);
return client.status;
matches = true;
}

if (hasVersionMismatch)
else
{
if (attemptAutoRewrite)
bool configuredIsBeta = IsBetaPackageSource(configured);
bool expectedIsBeta = IsBetaPackageSource(expected);

hasVersionMismatch = true;
if (configuredIsBeta && !expectedIsBeta)
{
string result = McpConfigurationHelper.ConfigureCodexClient(path, client);
if (result == "Configured successfully")
{
client.SetStatus(McpStatus.Configured);
client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport();
return client.status;
}
mismatchReason = "Configured for prerelease server, but this package is stable. Re-configure to switch to stable.";
}
else if (!configuredIsBeta && expectedIsBeta)
{
mismatchReason = "Configured for stable server, but this package is prerelease. Re-configure to switch to prerelease.";
}
else
{
mismatchReason = "Server version doesn't match the plugin. Re-configure to update.";
}
client.SetStatus(McpStatus.VersionMismatch, mismatchReason);
return client.status;
}
}
else
{
client.configuredTransport = Models.ConfiguredTransport.Unknown;
}
}

if (matches)
{
client.SetStatus(McpStatus.Configured);
return client.status;
}

if (hasVersionMismatch)
{
if (attemptAutoRewrite)
{
string result = McpConfigurationHelper.ConfigureCodexClient(path, client);
if (result == "Configured successfully")
{
client.SetStatus(McpStatus.Configured);
client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport();
}
else
{
client.SetStatus(McpStatus.IncorrectPath);
return client.status;
}
}
client.SetStatus(McpStatus.VersionMismatch, mismatchReason);
return client.status;
}

if (attemptAutoRewrite)
{
string result = McpConfigurationHelper.ConfigureCodexClient(path, client);
if (result == "Configured successfully")
{
client.SetStatus(McpStatus.Configured);
client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport();
}
else
{
client.SetStatus(McpStatus.IncorrectPath);
}
}
catch (Exception ex)
else
{
client.SetStatus(McpStatus.Error, ex.Message);
client.configuredTransport = Models.ConfiguredTransport.Unknown;
client.SetStatus(McpStatus.IncorrectPath);
}

return client.status;
}

public override void Configure()
{
string path = GetConfigPath();
string path = GetPreferredWriteConfigPath();
McpConfigurationHelper.EnsureConfigDirectoryExists(path);
string result = McpConfigurationHelper.ConfigureCodexClient(path, client);
if (result == "Configured successfully")
Expand All @@ -594,6 +603,34 @@ public override void Configure()
}
}

public override void Unregister()
{
try
{
foreach (string path in GetCandidateConfigPaths())
{
if (string.IsNullOrEmpty(path) || !File.Exists(path))
{
continue;
}

string toml = File.ReadAllText(path);
string updatedToml = CodexConfigHelper.RemoveCodexServerBlock(toml, out bool removed);
if (removed)
{
File.WriteAllText(path, updatedToml);
}
}

client.SetStatus(McpStatus.NotConfigured);
client.configuredTransport = Models.ConfiguredTransport.Unknown;
}
catch (Exception ex)
{
throw new InvalidOperationException($"Failed to unregister: {ex.Message}", ex);
}
}

public override string GetManualSnippet()
{
try
Expand All @@ -613,6 +650,55 @@ public override string GetManualSnippet()
"Paste the TOML",
"Save and restart Codex"
};

protected virtual string GetProjectConfigPath()
{
string projectDir = GetClientProjectDir();
return string.IsNullOrEmpty(projectDir)
? null
: Path.Combine(projectDir, ".codex", "config.toml");
}

private string GetUserConfigPath() => CurrentOsPath();

private string GetPreferredWriteConfigPath()
{
string projectPath = GetProjectConfigPath();
return string.IsNullOrEmpty(projectPath) ? GetUserConfigPath() : projectPath;
}

private IEnumerable<string> GetCandidateConfigPaths()
{
string projectPath = GetProjectConfigPath();
string userPath = GetUserConfigPath();

if (!string.IsNullOrEmpty(projectPath)) yield return projectPath;
if (!string.IsNullOrEmpty(userPath) && !PathsEqual(projectPath, userPath)) yield return userPath;
}

private static bool PathsEqual(string a, string b)
{
if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return false;
try
{
return string.Equals(
Path.GetFullPath(a).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar),
Path.GetFullPath(b).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar),
StringComparison.OrdinalIgnoreCase);
}
catch
{
return string.Equals(a, b, StringComparison.OrdinalIgnoreCase);
}
}

private static string GetClientProjectDir()
{
string overrideDir = EditorPrefs.GetString(EditorPrefKeys.ClientProjectDirOverride, string.Empty);
if (!string.IsNullOrEmpty(overrideDir) && Directory.Exists(overrideDir))
return overrideDir;
return Path.GetDirectoryName(Application.dataPath);
}
}

/// <summary>CLI-based configurator (Claude Code).</summary>
Expand Down
35 changes: 35 additions & 0 deletions MCPForUnity/Editor/Helpers/CodexConfigHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,29 @@ public static string UpsertCodexServerBlock(string existingToml, string uvPath)
return writer.ToString();
}

public static string RemoveCodexServerBlock(string existingToml, out bool removed)
{
removed = false;
var root = TryParseToml(existingToml);
if (root == null) return existingToml;

if (TryGetTable(root, "mcp_servers", out var snakeCaseServers))
{
removed |= RemoveUnityMcpServer(snakeCaseServers);
}

if (TryGetTable(root, "mcpServers", out var camelCaseServers))
{
removed |= RemoveUnityMcpServer(camelCaseServers);
}

if (!removed) return existingToml;

using var writer = new StringWriter();
root.WriteTo(writer);
return writer.ToString();
}

public static bool TryParseCodexServer(string toml, out string command, out string[] args)
{
return TryParseCodexServer(toml, out command, out args, out _);
Expand Down Expand Up @@ -272,6 +295,18 @@ private static bool TryGetTable(TomlTable parent, string key, out TomlTable tabl
return false;
}

private static bool RemoveUnityMcpServer(TomlTable servers)
{
if (servers == null) return false;

string key = servers.Keys.FirstOrDefault(k =>
string.Equals(k, "unityMCP", StringComparison.OrdinalIgnoreCase));
if (string.IsNullOrEmpty(key)) return false;

servers.Delete(key);
return true;
}

private static string GetTomlString(TomlTable table, string key)
{
if (table != null && table.TryGetNode(key, out var node))
Expand Down
Loading