diff --git a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs index 74fd836f7..a245f1aea 100644 --- a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs +++ b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs @@ -436,121 +436,122 @@ 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); @@ -558,21 +559,29 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true) { 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; @@ -580,7 +589,7 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true) public override void Configure() { - string path = GetConfigPath(); + string path = GetPreferredWriteConfigPath(); McpConfigurationHelper.EnsureConfigDirectoryExists(path); string result = McpConfigurationHelper.ConfigureCodexClient(path, client); if (result == "Configured successfully") @@ -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 @@ -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 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); + } } /// CLI-based configurator (Claude Code). diff --git a/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs b/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs index 45ae5a39e..7aa0f95cf 100644 --- a/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs +++ b/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs @@ -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 _); @@ -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)) diff --git a/MCPForUnity/Editor/Services/StartupConfigRewrite.cs b/MCPForUnity/Editor/Services/StartupConfigRewrite.cs index 2df69f3a3..c850f81ea 100644 --- a/MCPForUnity/Editor/Services/StartupConfigRewrite.cs +++ b/MCPForUnity/Editor/Services/StartupConfigRewrite.cs @@ -9,10 +9,8 @@ namespace MCPForUnity.Editor.Services { /// - /// Once per Editor session, sweeps registered configurators and re-runs CheckStatus(attemptAutoRewrite: true) - /// for any installed client that already has a config on disk. Catches the case where the user updated the - /// MCP for Unity package while the Editor was closed — without this sweep, stale package versions in client - /// configs would persist until the user opens the MCP window. + /// Once per Editor session, sweeps registered configurators and refreshes existing UnityMCP entries that + /// drifted while the Editor was closed. Missing registrations stay user-driven through Configure buttons. /// [InitializeOnLoad] public static class StartupConfigRewrite @@ -43,10 +41,14 @@ private static void RunOnce() try { if (!c.IsInstalled) continue; - // Always let CheckStatus read the current state from disk before deciding — + // Always let CheckStatus read the current state from disk before deciding. // the in-memory Status can be NotConfigured on a fresh editor load even // though the file already has a valid config. var before = c.Status; + var current = c.CheckStatus(attemptAutoRewrite: false); + if (current != McpStatus.VersionMismatch && current != McpStatus.IncorrectPath) + continue; + var after = c.CheckStatus(attemptAutoRewrite: true); if (before != after && after == McpStatus.Configured) rewrote++; }