diff --git a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs index 7b7f9b8cd..4deb4135a 100644 --- a/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs +++ b/MCPForUnity/Editor/Clients/McpClientConfiguratorBase.cs @@ -353,8 +353,13 @@ public override string GetManualSnippet() /// Codex (TOML) configurator. public abstract class CodexMcpConfigurator : McpClientConfiguratorBase { + private const string CodexServerName = "unityMCP"; + private const string LegacyCodexServerName = "UnityMCP"; + public CodexMcpConfigurator(McpClient client) : base(client) { } + public override string GetConfigureActionLabel() => client.status == McpStatus.Configured ? "Unregister" : "Configure"; + public override string GetConfigPath() => CurrentOsPath(); public override McpStatus CheckStatus(bool attemptAutoRewrite = true) @@ -403,6 +408,16 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true) { // Match against the active scope's URL matches = UrlsEqual(url, HttpEndpointUtility.GetMcpRpcUrl()); + if (matches && ShouldUseTomlForRemoteAuth()) + { + string apiKey = EditorPrefs.GetString(EditorPrefKeys.ApiKey, string.Empty); + if (!CodexConfigHelper.HasCodexHttpHeader(toml, AuthConstants.ApiKeyHeader, apiKey)) + { + matches = false; + hasVersionMismatch = true; + mismatchReason = "Remote auth header is missing or stale. Re-configure to update Codex."; + } + } } else if (args != null && args.Length > 0) { @@ -451,13 +466,8 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true) { if (attemptAutoRewrite) { - string result = McpConfigurationHelper.ConfigureCodexClient(path, client); - if (result == "Configured successfully") - { - client.SetStatus(McpStatus.Configured); - client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport(); - return client.status; - } + Register(); + return client.status; } client.SetStatus(McpStatus.VersionMismatch, mismatchReason); return client.status; @@ -470,16 +480,7 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true) 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); - } + Register(); } else { @@ -497,17 +498,13 @@ public override McpStatus CheckStatus(bool attemptAutoRewrite = true) public override void Configure() { - string path = GetConfigPath(); - McpConfigurationHelper.EnsureConfigDirectoryExists(path); - string result = McpConfigurationHelper.ConfigureCodexClient(path, client); - if (result == "Configured successfully") + if (client.status == McpStatus.Configured) { - client.SetStatus(McpStatus.Configured); - client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport(); + Unregister(); } else { - throw new InvalidOperationException(result); + Register(); } } @@ -515,8 +512,36 @@ public override string GetManualSnippet() { try { - string uvx = GetUvxPathOrError(); - return CodexConfigHelper.BuildCodexServerBlock(uvx); + if (ShouldUseTomlForRemoteAuth()) + { + return "# Codex CLI does not currently expose an arbitrary HTTP header flag.\n" + + "# For remote-hosted servers with X-API-Key auth, add this TOML to ~/.codex/config.toml:\n" + + CodexConfigHelper.BuildCodexServerBlock(null); + } + + bool useHttpTransport = EditorConfigurationCache.Instance.UseHttpTransport; + if (useHttpTransport) + { + string httpUrl = HttpEndpointUtility.GetMcpRpcUrl(); + return "# Register the MCP server with Codex:\n" + + $"codex mcp add {CodexServerName} --url {QuoteCliArg(httpUrl)}\n\n" + + "# Unregister the MCP server:\n" + + $"codex mcp remove {CodexServerName}\n\n" + + "# List configured servers:\n" + + "codex mcp list"; + } + + var (uvxPath, _, packageName) = AssetPathUtility.GetUvxCommandParts(); + string devFlags = AssetPathUtility.GetUvxDevFlags(); + string fromArgs = AssetPathUtility.GetBetaServerFromArgs(quoteFromPath: true); + string envArg = GetCodexStdioEnvArg(); + + return "# Register the MCP server with Codex:\n" + + $"codex mcp add {CodexServerName}{envArg} -- {QuoteCliArg(uvxPath)} {devFlags}{fromArgs} {packageName} --transport stdio\n\n" + + "# Unregister the MCP server:\n" + + $"codex mcp remove {CodexServerName}\n\n" + + "# List configured servers:\n" + + "codex mcp list"; } catch (Exception ex) { @@ -526,10 +551,229 @@ public override string GetManualSnippet() public override IList GetInstallationSteps() => new List { - "Run 'codex config edit' or open the config path", - "Paste the TOML", - "Save and restart Codex" + "Ensure the Codex CLI is installed", + "Click Configure to add Unity MCP via 'codex mcp add'", + "Codex reads the configuration from ~/.codex/config.toml", + "Use Unregister to remove it via 'codex mcp remove'" }; + + private void Register() + { + if (ShouldUseTomlForRemoteAuth()) + { + RegisterWithToml(); + return; + } + + WarnIfRemoteHttpHasNoApiKey(); + + string codexPath = ResolveCodexCliPath(); + if (string.IsNullOrEmpty(codexPath)) + { + McpLog.Warn("Codex CLI not found. Falling back to ~/.codex/config.toml."); + RegisterWithToml(); + return; + } + + string args = BuildCodexAddArgs(); + + RemoveCodexRegistrations(codexPath); + + if (!ExecPath.TryRun(codexPath, args, null, out var stdout, out var stderr, 15000, GetCodexPathPrepend(codexPath))) + { + McpLog.Warn($"Codex CLI registration failed. Falling back to ~/.codex/config.toml.\n{stderr}\n{stdout}"); + RegisterWithToml(); + return; + } + + client.SetStatus(McpStatus.Configured); + client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport(); + McpLog.Info($"Successfully registered with Codex using {(EditorConfigurationCache.Instance.UseHttpTransport ? "HTTP" : "stdio")} transport."); + } + + private void RegisterWithToml() + { + string path = GetConfigPath(); + McpConfigurationHelper.EnsureConfigDirectoryExists(path); + string result = McpConfigurationHelper.ConfigureCodexClient(path, client); + if (result != "Configured successfully") + { + throw new InvalidOperationException(result); + } + + client.SetStatus(McpStatus.Configured); + client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport(); + } + + private void Unregister() + { + string codexPath = ResolveCodexCliPath(); + if (!string.IsNullOrEmpty(codexPath)) + { + RemoveCodexRegistrations(codexPath); + } + + RemoveCodexTomlRegistration(); + client.SetStatus(McpStatus.NotConfigured); + client.configuredTransport = Models.ConfiguredTransport.Unknown; + McpLog.Info("MCP server successfully unregistered from Codex."); + } + + private string BuildCodexAddArgs() + { + bool useHttpTransport = EditorConfigurationCache.Instance.UseHttpTransport; + if (useHttpTransport) + { + string httpUrl = HttpEndpointUtility.GetMcpRpcUrl(); + return $"mcp add {CodexServerName} --url {QuoteCliArg(httpUrl)}"; + } + + var (uvxPath, _, packageName) = AssetPathUtility.GetUvxCommandParts(); + string devFlags = AssetPathUtility.GetUvxDevFlags(); + string fromArgs = AssetPathUtility.GetBetaServerFromArgs(quoteFromPath: true); + string envArg = GetCodexStdioEnvArg(); + + return $"mcp add {CodexServerName}{envArg} -- {QuoteCliArg(uvxPath)} {devFlags}{fromArgs} {packageName} --transport stdio"; + } + + private static bool ShouldUseTomlForRemoteAuth() + { + return EditorConfigurationCache.Instance.UseHttpTransport + && HttpEndpointUtility.IsRemoteScope() + && !string.IsNullOrEmpty(EditorPrefs.GetString(EditorPrefKeys.ApiKey, string.Empty)); + } + + private static void WarnIfRemoteHttpHasNoApiKey() + { + if (!EditorConfigurationCache.Instance.UseHttpTransport || !HttpEndpointUtility.IsRemoteScope()) + { + return; + } + + if (!string.IsNullOrEmpty(EditorPrefs.GetString(EditorPrefKeys.ApiKey, string.Empty))) + { + return; + } + + McpLog.Warn("Codex is being configured for a remote HTTP MCP server without an API key. If that server requires X-API-Key authentication, add the key in MCP for Unity before configuring Codex."); + } + + private static string GetCodexStdioEnvArg() + { + if (Application.platform != RuntimePlatform.WindowsEditor) + { + return string.Empty; + } + + string systemRoot = MCPServiceLocator.Platform.GetSystemRoot(); + return string.IsNullOrEmpty(systemRoot) + ? string.Empty + : $" --env {QuoteCliArg($"SystemRoot={systemRoot}")}"; + } + + private void RemoveCodexRegistrations(string codexPath) + { + string pathPrepend = GetCodexPathPrepend(codexPath); + ExecPath.TryRun(codexPath, $"mcp remove {CodexServerName}", null, out _, out _, 5000, pathPrepend); + ExecPath.TryRun(codexPath, $"mcp remove {LegacyCodexServerName}", null, out _, out _, 5000, pathPrepend); + } + + private void RemoveCodexTomlRegistration() + { + string path = GetConfigPath(); + if (!File.Exists(path)) return; + + string existingToml = File.ReadAllText(path); + string updatedToml = CodexConfigHelper.RemoveCodexServerBlock(existingToml); + if (!string.Equals(existingToml, updatedToml, StringComparison.Ordinal)) + { + McpConfigurationHelper.WriteAtomicFile(path, updatedToml); + } + } + + private static string ResolveCodexCliPath() + { + string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) ?? string.Empty; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) ?? string.Empty; + string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) ?? string.Empty; + string[] candidates = + { + Path.Combine(appData, "npm", "codex.cmd"), + Path.Combine(appData, "npm", "codex.ps1"), + Path.Combine(localAppData, "npm", "codex.cmd"), + Path.Combine(localAppData, "npm", "codex.ps1"), + Path.Combine(home, ".local", "bin", "codex.exe"), + }; + + foreach (string candidate in candidates) + { + if (File.Exists(candidate)) return candidate; + } + + foreach (string name in new[] { "codex.exe", "codex.cmd", "codex.ps1", "codex" }) + { + string fromPath = ExecPath.FindInPath(name); + if (!string.IsNullOrEmpty(fromPath)) return fromPath; + } + } + else + { + string[] candidates = + { + "/opt/homebrew/bin/codex", + "/usr/local/bin/codex", + "/usr/bin/codex", + Path.Combine(home, ".local", "bin", "codex"), + Path.Combine(home, ".npm-global", "bin", "codex"), + }; + + foreach (string candidate in candidates) + { + if (File.Exists(candidate)) return candidate; + } + + string fromPath = ExecPath.FindInPath("codex", GetDefaultCliPathPrepend()); + if (!string.IsNullOrEmpty(fromPath)) return fromPath; + } + + return null; + } + + private static string GetCodexPathPrepend(string codexPath) + { + string pathPrepend = GetDefaultCliPathPrepend(); + try + { + string codexDir = Path.GetDirectoryName(codexPath); + if (!string.IsNullOrEmpty(codexDir)) + { + pathPrepend = string.IsNullOrEmpty(pathPrepend) + ? codexDir + : $"{codexDir}{Path.PathSeparator}{pathPrepend}"; + } + } + catch { } + + return pathPrepend; + } + + private static string GetDefaultCliPathPrepend() + { + if (Application.platform == RuntimePlatform.OSXEditor) + return "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"; + if (Application.platform == RuntimePlatform.LinuxEditor) + return "/usr/local/bin:/usr/bin:/bin"; + return null; + } + + private static string QuoteCliArg(string value) + { + if (string.IsNullOrEmpty(value)) return "\"\""; + return "\"" + value.Replace("\"", "\\\"") + "\""; + } } /// CLI-based configurator (Claude Code). diff --git a/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs b/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs index 45ae5a39e..e0c0f220a 100644 --- a/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs +++ b/MCPForUnity/Editor/Helpers/CodexConfigHelper.cs @@ -39,6 +39,8 @@ public static string BuildCodexServerBlock(string uvPath) string httpUrl = HttpEndpointUtility.GetMcpRpcUrl(); unityMCP["url"] = new TomlString { Value = httpUrl }; + AddRemoteAuthHeaderIfNeeded(unityMCP); + // Enable Codex's Rust MCP client for HTTP/SSE transport EnsureRmcpClientFeature(table); } @@ -83,6 +85,26 @@ public static string BuildCodexServerBlock(string uvPath) return writer.ToString(); } + public static string RemoveCodexServerBlock(string existingToml) + { + var root = TryParseToml(existingToml); + if (root == null) + { + if (!string.IsNullOrWhiteSpace(existingToml)) + { + McpLog.Warn("Codex config.toml could not be parsed; leaving it unchanged. Manual cleanup of the [mcp_servers.unityMCP] block may be required."); + } + return existingToml ?? string.Empty; + } + + RemoveUnityServer(root, "mcp_servers"); + RemoveUnityServer(root, "mcpServers"); + + using var writer = new StringWriter(); + root.WriteTo(writer); + return writer.ToString(); + } + public static string UpsertCodexServerBlock(string existingToml, string uvPath) { // Parse existing TOML or create new root table @@ -151,6 +173,34 @@ public static bool TryParseCodexServer(string toml, out string command, out stri return !string.IsNullOrEmpty(command) && args != null; } + public static bool HasCodexHttpHeader(string toml, string headerName, string expectedValue) + { + if (string.IsNullOrEmpty(headerName)) return false; + + var root = TryParseToml(toml); + if (root == null) return false; + + if (!TryGetTable(root, "mcp_servers", out var servers) + && !TryGetTable(root, "mcpServers", out servers)) + { + return false; + } + + if (!TryGetTable(servers, "unityMCP", out var unity) + && !TryGetTable(servers, "UnityMCP", out unity)) + { + return false; + } + + if (!TryGetTable(unity, "http_headers", out var headers)) + { + return false; + } + + string configuredValue = GetTomlString(headers, headerName); + return string.Equals(configuredValue, expectedValue, StringComparison.Ordinal); + } + /// /// Safely parses TOML string, returning null on failure /// @@ -193,6 +243,7 @@ private static TomlTable CreateUnityMcpTable(string uvPath) // HTTP mode: Use url field string httpUrl = HttpEndpointUtility.GetMcpRpcUrl(); unityMCP["url"] = new TomlString { Value = httpUrl }; + AddRemoteAuthHeaderIfNeeded(unityMCP); } else { @@ -229,6 +280,48 @@ private static TomlTable CreateUnityMcpTable(string uvPath) return unityMCP; } + private static void AddRemoteAuthHeaderIfNeeded(TomlTable unityMCP) + { + if (unityMCP == null) return; + + if (!HttpEndpointUtility.IsRemoteScope()) + { + unityMCP.Delete("http_headers"); + return; + } + + string apiKey = EditorPrefs.GetString(EditorPrefKeys.ApiKey, string.Empty); + if (string.IsNullOrEmpty(apiKey)) + { + unityMCP.Delete("http_headers"); + return; + } + + var headers = new TomlTable { IsInline = true }; + headers[AuthConstants.ApiKeyHeader] = new TomlString { Value = apiKey }; + unityMCP["http_headers"] = headers; + } + + private static void RemoveUnityServer(TomlTable root, string serverTableName) + { + if (root == null) return; + if (!TryGetTable(root, serverTableName, out var servers)) return; + + foreach (string key in servers.Keys.ToList()) + { + if (string.Equals(key, "unityMCP", StringComparison.OrdinalIgnoreCase) + || string.Equals(key, "UnityMCP", StringComparison.OrdinalIgnoreCase)) + { + servers.Delete(key); + } + } + + if (servers.ChildrenCount == 0) + { + root.Delete(serverTableName); + } + } + /// /// Ensures the features table contains the rmcp_client flag for HTTP/SSE transport. /// diff --git a/MCPForUnity/README.md b/MCPForUnity/README.md index 3631f60f6..91ee0640d 100644 --- a/MCPForUnity/README.md +++ b/MCPForUnity/README.md @@ -16,8 +16,9 @@ The window has four areas: Server Status, Unity Bridge, MCP Client Configuration - Select the packaged server folder (`Server`) if you want to run the bundled implementation. - Install Python and/or uv/uvx if missing so the server can be managed locally. - For Claude Code, ensure the `claude` CLI is installed. + - For Codex, ensure the `codex` CLI is installed. 4. Click “Start Bridge” if the Unity Bridge shows “Stopped”. -5. Use your MCP client (Cursor, VS Code, OpenClaw, Claude Code) to connect. +5. Use your MCP client (Cursor, VS Code, OpenClaw, Claude Code, Codex) to connect. --- @@ -47,7 +48,7 @@ The window has four areas: Server Status, Unity Bridge, MCP Client Configuration --- ## MCP Client Configuration -- Select Client: Choose your target MCP client (e.g., Cursor, VS Code, Windsurf, Claude Code). +- Select Client: Choose your target MCP client (e.g., Cursor, VS Code, Windsurf, Claude Code, Codex). - Per-client actions: - Cursor / VS Code / Windsurf: - Auto Configure: Writes/updates your config to launch the server via `uvx` with the current package version: @@ -60,6 +61,11 @@ The window has four areas: Server Status, Unity Bridge, MCP Client Configuration - Register with Claude Code / Unregister MCP for Unity with Claude Code. - If the CLI isn’t found, click “Choose Claude Install Location”. - The window displays the resolved Claude CLI path when detected. + - Codex: + - Configure / Unregister MCP for Unity with Codex. + - Local HTTP and stdio setup use `codex mcp add` / `codex mcp remove` when the Codex CLI is available. + - If the CLI is not found, MCP for Unity writes `~/.codex/config.toml` directly. + - Remote-hosted setup with `X-API-Key` auth uses TOML because the Codex CLI does not currently expose an arbitrary HTTP header flag. - OpenClaw: - Uses `~/.openclaw/openclaw.json` and the `openclaw-mcp-bridge` plugin. - MCP for Unity writes `plugins.entries.openclaw-mcp-bridge.config.servers.unityMCP`. @@ -87,6 +93,8 @@ Notes: - Help: [Fix MCP for Unity with Cursor, VS Code & Windsurf](https://github.com/CoplayDev/unity-mcp/wiki/1.-Fix-Unity-MCP-and-Cursor,-VSCode-&-Windsurf) - Claude CLI not found: - Help: [Fix MCP for Unity with Claude Code](https://github.com/CoplayDev/unity-mcp/wiki/2.-Fix-Unity-MCP-and-Claude-Code) +- Codex not connecting: + - Help: [Codex setup help](../docs/guides/CODEX_HELP.md) --- diff --git a/README.md b/README.md index 5a47a6f87..af201c9fd 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ [![](https://badge.mcpx.dev?status=on 'MCP Enabled')](https://modelcontextprotocol.io/introduction) [![](https://img.shields.io/badge/License-MIT-red.svg 'MIT License')](https://opensource.org/licenses/MIT) -**Create your Unity apps with LLMs!** MCP for Unity bridges AI assistants (Claude, Claude Code, Cursor, VS Code, etc.) with your Unity Editor via the [Model Context Protocol](https://modelcontextprotocol.io/introduction). Give your LLM the tools to manage assets, control scenes, edit scripts, and automate tasks. +**Create your Unity apps with LLMs!** MCP for Unity bridges AI assistants (Claude, Claude Code, Codex, Cursor, VS Code, etc.) with your Unity Editor via the [Model Context Protocol](https://modelcontextprotocol.io/introduction). Give your LLM the tools to manage assets, control scenes, edit scripts, and automate tasks. MCP for Unity building a scene @@ -47,7 +47,7 @@ * **Unity 2021.3 LTS+** — [Download Unity](https://unity.com/download) * **Python 3.10+** and **uv** — [Install uv](https://docs.astral.sh/uv/getting-started/installation/) -* **An MCP Client** — [Claude Desktop](https://claude.ai/download) | [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | [Cursor](https://www.cursor.com/en/downloads) | [VS Code Copilot](https://code.visualstudio.com/docs/copilot/overview) | [GitHub Copilot CLI](https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli) | [OpenClaw](https://openclaw.ai) +* **An MCP Client** — [Claude Desktop](https://claude.ai/download) | [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | [Codex](https://developers.openai.com/codex/mcp) | [Cursor](https://www.cursor.com/en/downloads) | [VS Code Copilot](https://code.visualstudio.com/docs/copilot/overview) | [GitHub Copilot CLI](https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli) | [OpenClaw](https://openclaw.ai) ### 1. Install the Unity Package @@ -82,7 +82,7 @@ openupm add com.coplaydev.unity-mcp 2. Click **Start Server** (launches HTTP server on `localhost:8080`) 3. Select your MCP Client from the dropdown and click **Configure** 4. Look for 🟢 "Connected ✓" -5. **Connect your client:** Some clients (Cursor, Antigravity, OpenClaw) require enabling an MCP toggle or plugin in settings. OpenClaw also needs the `openclaw-mcp-bridge` plugin enabled and follows the currently selected MCP for Unity transport (`HTTP` or `stdio`). Others (Claude Desktop, Claude Code) auto-connect after configuration. +5. **Connect your client:** Some clients (Cursor, Antigravity, OpenClaw) require enabling an MCP toggle or plugin in settings. OpenClaw also needs the `openclaw-mcp-bridge` plugin enabled and follows the currently selected MCP for Unity transport (`HTTP` or `stdio`). Others (Claude Desktop, Claude Code, Codex) pick up the server after configuration or in the next session. **That's it!** Try a prompt like: *"Create a red, blue and yellow cube"* or *"Build a simple player controller"* @@ -134,6 +134,14 @@ If auto-setup doesn't work, add this to your MCP client's config file: } ``` +**Codex CLI:** +```bash +codex mcp add unityMCP --url http://127.0.0.1:8080/mcp +codex mcp list +``` + +Codex stores MCP servers in `~/.codex/config.toml`. If you need remote-hosted mode with `X-API-Key` auth, use the Unity Editor's Configure button or see [CODEX_HELP.md](docs/guides/CODEX_HELP.md) for the TOML form. +
Stdio configuration (uvx) @@ -213,6 +221,7 @@ For **Strict** validation that catches undefined namespaces, types, and methods: **Detailed setup guides:** * [Fix Unity MCP and Cursor, VSCode & Windsurf](https://github.com/CoplayDev/unity-mcp/wiki/1.-Fix-Unity-MCP-and-Cursor,-VSCode-&-Windsurf) — uv/Python installation, PATH issues * [Fix Unity MCP and Claude Code](https://github.com/CoplayDev/unity-mcp/wiki/2.-Fix-Unity-MCP-and-Claude-Code) — Claude CLI installation +* [Codex setup help](docs/guides/CODEX_HELP.md) — Codex CLI registration, TOML fallback, and verification * [Common Setup Problems](https://github.com/CoplayDev/unity-mcp/wiki/3.-Common-Setup-Problems) — macOS dyld errors, FAQ Still stuck? [Open an Issue](https://github.com/CoplayDev/unity-mcp/issues) or [Join Discord](https://discord.gg/y4p8KfzrN4) diff --git a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/CodexConfigHelperTests.cs b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/CodexConfigHelperTests.cs index 32a5aef37..69ddbac32 100644 --- a/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/CodexConfigHelperTests.cs +++ b/TestProjects/UnityMCPTests/Assets/Tests/EditMode/Helpers/CodexConfigHelperTests.cs @@ -59,6 +59,12 @@ public MockPlatformService(bool isWindows, string systemRoot = "C:\\Windows") private string _originalGitOverride; private bool _hadHttpTransport; private bool _originalHttpTransport; + private bool _hadHttpTransportScope; + private string _originalHttpTransportScope; + private bool _hadHttpRemoteBaseUrl; + private string _originalHttpRemoteBaseUrl; + private bool _hadApiKey; + private string _originalApiKey; private bool _hadDevForceRefresh; private bool _originalDevForceRefresh; private IPlatformService _originalPlatformService; @@ -70,6 +76,12 @@ public void OneTimeSetUp() _originalGitOverride = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, string.Empty); _hadHttpTransport = EditorPrefs.HasKey(EditorPrefKeys.UseHttpTransport); _originalHttpTransport = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true); + _hadHttpTransportScope = EditorPrefs.HasKey(EditorPrefKeys.HttpTransportScope); + _originalHttpTransportScope = EditorPrefs.GetString(EditorPrefKeys.HttpTransportScope, string.Empty); + _hadHttpRemoteBaseUrl = EditorPrefs.HasKey(EditorPrefKeys.HttpRemoteBaseUrl); + _originalHttpRemoteBaseUrl = EditorPrefs.GetString(EditorPrefKeys.HttpRemoteBaseUrl, string.Empty); + _hadApiKey = EditorPrefs.HasKey(EditorPrefKeys.ApiKey); + _originalApiKey = EditorPrefs.GetString(EditorPrefKeys.ApiKey, string.Empty); _hadDevForceRefresh = EditorPrefs.HasKey(EditorPrefKeys.DevModeForceServerRefresh); _originalDevForceRefresh = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false); _originalPlatformService = MCPServiceLocator.Platform; @@ -82,6 +94,9 @@ public void SetUp() EditorPrefs.DeleteKey(EditorPrefKeys.GitUrlOverride); // Default to stdio mode for existing tests unless specified otherwise EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, false); + EditorPrefs.DeleteKey(EditorPrefKeys.HttpTransportScope); + EditorPrefs.DeleteKey(EditorPrefKeys.HttpRemoteBaseUrl); + EditorPrefs.DeleteKey(EditorPrefKeys.ApiKey); // Ensure deterministic uvx args ordering for these tests regardless of editor settings // (dev-mode inserts --no-cache/--refresh, which changes the first args). EditorPrefs.SetBool(EditorPrefKeys.DevModeForceServerRefresh, false); @@ -129,6 +144,33 @@ public void OneTimeTearDown() EditorPrefs.DeleteKey(EditorPrefKeys.UseHttpTransport); } + if (_hadHttpTransportScope) + { + EditorPrefs.SetString(EditorPrefKeys.HttpTransportScope, _originalHttpTransportScope); + } + else + { + EditorPrefs.DeleteKey(EditorPrefKeys.HttpTransportScope); + } + + if (_hadHttpRemoteBaseUrl) + { + EditorPrefs.SetString(EditorPrefKeys.HttpRemoteBaseUrl, _originalHttpRemoteBaseUrl); + } + else + { + EditorPrefs.DeleteKey(EditorPrefKeys.HttpRemoteBaseUrl); + } + + if (_hadApiKey) + { + EditorPrefs.SetString(EditorPrefKeys.ApiKey, _originalApiKey); + } + else + { + EditorPrefs.DeleteKey(EditorPrefKeys.ApiKey); + } + if (_hadDevForceRefresh) { EditorPrefs.SetBool(EditorPrefKeys.DevModeForceServerRefresh, _originalDevForceRefresh); @@ -511,6 +553,43 @@ public void BuildCodexServerBlock_HttpMode_GeneratesUrlField() Assert.IsFalse(unityMcp.TryGetNode("command", out _), "HTTP mode should not contain command field"); Assert.IsFalse(unityMcp.TryGetNode("args", out _), "HTTP mode should not contain args field"); Assert.IsFalse(unityMcp.TryGetNode("env", out _), "HTTP mode should not contain env field"); + Assert.IsFalse(unityMcp.TryGetNode("http_headers", out _), "Local HTTP mode should not contain auth headers"); + } + + [Test] + public void BuildCodexServerBlock_RemoteHttpModeWithApiKey_IncludesHttpHeaders() + { + EditorPrefs.SetBool(EditorPrefKeys.UseHttpTransport, true); + EditorPrefs.SetString(EditorPrefKeys.HttpTransportScope, "remote"); + EditorPrefs.SetString(EditorPrefKeys.HttpRemoteBaseUrl, "https://unity-mcp.example"); + EditorPrefs.SetString(EditorPrefKeys.ApiKey, "test-api-key"); + EditorConfigurationCache.Instance.Refresh(); + + string result = CodexConfigHelper.BuildCodexServerBlock("uvx"); + + TomlTable parsed; + using (var reader = new StringReader(result)) + { + parsed = TOML.Parse(reader); + } + + var mcpServers = parsed["mcp_servers"] as TomlTable; + var unityMcp = mcpServers["unityMCP"] as TomlTable; + + Assert.IsTrue(unityMcp.TryGetNode("url", out var urlNode), "Remote HTTP mode should contain url"); + Assert.AreEqual("https://unity-mcp.example/mcp", (urlNode as TomlString).Value); + + Assert.IsTrue(unityMcp.TryGetNode("http_headers", out var headersNode), "Remote HTTP mode should include auth headers"); + Assert.IsInstanceOf(headersNode); + var headers = headersNode as TomlTable; + Assert.IsTrue(headers.TryGetNode(AuthConstants.ApiKeyHeader, out var apiKeyNode), "Headers should contain X-API-Key"); + Assert.AreEqual("test-api-key", (apiKeyNode as TomlString).Value); + Assert.IsTrue( + CodexConfigHelper.HasCodexHttpHeader(result, AuthConstants.ApiKeyHeader, "test-api-key"), + "Header helper should detect the generated X-API-Key"); + Assert.IsFalse( + CodexConfigHelper.HasCodexHttpHeader(result, AuthConstants.ApiKeyHeader, "stale-key"), + "Header helper should reject stale X-API-Key values"); } [Test] @@ -591,5 +670,36 @@ public void UpsertCodexServerBlock_HttpMode_GeneratesUrlField() Assert.IsFalse(unityMcp.TryGetNode("command", out _), "HTTP mode should not contain command field"); Assert.IsFalse(unityMcp.TryGetNode("args", out _), "HTTP mode should not contain args field"); } + + [Test] + public void RemoveCodexServerBlock_RemovesUnityServerAndPreservesOtherSections() + { + string existingToml = string.Join("\n", new[] + { + "[profile.default]", + "model = \"gpt-5.2-codex\"", + "", + "[mcp_servers.unityMCP]", + "url = \"http://127.0.0.1:8080/mcp\"", + "", + "[mcp_servers.otherServer]", + "url = \"http://127.0.0.1:9999/mcp\"" + }); + + string result = CodexConfigHelper.RemoveCodexServerBlock(existingToml); + + TomlTable parsed; + using (var reader = new StringReader(result)) + { + parsed = TOML.Parse(reader); + } + + Assert.IsTrue(parsed.TryGetNode("profile", out _), "Unrelated sections should be preserved"); + Assert.IsTrue(parsed.TryGetNode("mcp_servers", out var serversNode), "Other MCP servers should be preserved"); + + var servers = serversNode as TomlTable; + Assert.IsFalse(servers.TryGetNode("unityMCP", out _), "Unity MCP server should be removed"); + Assert.IsTrue(servers.TryGetNode("otherServer", out _), "Other MCP servers should remain"); + } } } diff --git a/docs/guides/CODEX_HELP.md b/docs/guides/CODEX_HELP.md new file mode 100644 index 000000000..dc66f6fc6 --- /dev/null +++ b/docs/guides/CODEX_HELP.md @@ -0,0 +1,87 @@ +### Codex: MCP for Unity setup + +Codex can use MCP for Unity through the Codex CLI or by reading `~/.codex/config.toml`. The Unity Editor window uses the CLI first for the common local cases and falls back to TOML when needed. + +## Quick setup + +1. Install Codex and confirm the CLI is available: + +```bash +codex --version +``` + +2. Open Unity and start MCP for Unity: + - `Window > MCP for Unity` + - Click `Start Server` + - Select `Codex` + - Click `Configure` + +3. Start a new Codex session and check the server: + +```bash +codex mcp list +codex mcp get unityMCP --json +``` + +For local HTTP, the generated entry should point at: + +```text +http://127.0.0.1:8080/mcp +``` + +`127.0.0.1` is preferred over `localhost` because some Codex HTTP clients resolve `localhost` to IPv6 first, while the local Unity MCP server binds to IPv4. + +## Manual local HTTP setup + +If the Unity Editor setup is not available, register the server from a terminal: + +```bash +codex mcp add unityMCP --url http://127.0.0.1:8080/mcp +codex mcp list +``` + +To remove it: + +```bash +codex mcp remove unityMCP +``` + +## Manual stdio setup + +Use stdio when you want Codex to launch the MCP server process itself: + +```bash +codex mcp add unityMCP -- uvx --from mcpforunityserver mcp-for-unity --transport stdio +``` + +On Windows, if Codex cannot launch the stdio server, add `SystemRoot`: + +```bash +codex mcp add unityMCP --env "SystemRoot=C:\Windows" -- uvx --from mcpforunityserver mcp-for-unity --transport stdio +``` + +If `uvx` is not on Codex's PATH, use the absolute `uvx` path shown in the MCP for Unity window. Quote paths that contain spaces. + +## Remote-hosted auth + +Codex supports HTTP MCP servers, but the Codex CLI does not currently expose a flag for arbitrary HTTP headers such as `X-API-Key`. For remote-hosted MCP for Unity servers that require this header, configure `~/.codex/config.toml` directly: + +```toml +[mcp_servers.unityMCP] +url = "https://your-server.example/mcp" +http_headers = { "X-API-Key" = "your-api-key" } +``` + +This stores the API key in `~/.codex/config.toml` under `[mcp_servers.unityMCP].http_headers`. Keep that file private and use restrictive file permissions where your platform supports them. + +The Unity Editor's `Configure` action writes this TOML form automatically when remote-hosted mode has an API key configured. + +## Troubleshooting + +If Codex does not show Unity tools: + +1. Confirm the MCP for Unity server is running in Unity. +2. Run `codex mcp list` and check that `unityMCP` is enabled. +3. Run `codex mcp get unityMCP --json` and verify the URL or stdio command. +4. Restart the Codex session after changing MCP configuration. +5. If local HTTP fails with `localhost`, use `http://127.0.0.1:8080/mcp`. diff --git a/docs/guides/MCP_CLIENT_CONFIGURATORS.md b/docs/guides/MCP_CLIENT_CONFIGURATORS.md index 09d8a0540..aebbe5dcf 100644 --- a/docs/guides/MCP_CLIENT_CONFIGURATORS.md +++ b/docs/guides/MCP_CLIENT_CONFIGURATORS.md @@ -136,11 +136,14 @@ Some clients cannot be handled by the generic JSON configurator alone. ### Codex (TOML-based) - Uses **`CodexMcpConfigurator`**. -- Reads and writes a **TOML** config (usually `~/.codex/config.toml`). +- Uses `codex mcp add` / `codex mcp remove` for local HTTP and stdio setup when the Codex CLI is available. +- Reads `~/.codex/config.toml` for fast status checks and to detect manual configuration. +- Falls back to writing TOML directly when the Codex CLI is not available. +- Writes TOML directly for remote-hosted `X-API-Key` auth because the Codex CLI does not currently expose an arbitrary HTTP header flag. - Uses `CodexConfigHelper` to: - Parse the existing TOML. - Check for a matching Unity MCP server configuration. - - Write/patch the Codex server block. + - Write/patch/remove the Codex server block. - The `CodexConfigurator` class: - Only needs to supply a `McpClient` with TOML config paths. - Inherits the Codex-specific status and configure behavior from `CodexMcpConfigurator`. diff --git a/docs/i18n/README-zh.md b/docs/i18n/README-zh.md index 87e451f4f..787bb9312 100644 --- a/docs/i18n/README-zh.md +++ b/docs/i18n/README-zh.md @@ -13,7 +13,7 @@ [![](https://badge.mcpx.dev?status=on 'MCP Enabled')](https://modelcontextprotocol.io/introduction) [![](https://img.shields.io/badge/License-MIT-red.svg 'MIT License')](https://opensource.org/licenses/MIT) -**用大语言模型创建你的 Unity 应用!** MCP for Unity 通过 [Model Context Protocol](https://modelcontextprotocol.io/introduction) 将 AI 助手(Claude、Cursor、VS Code 等)与你的 Unity Editor 连接起来。为你的大语言模型提供管理资源、控制场景、编辑脚本和自动化任务的工具。 +**用大语言模型创建你的 Unity 应用!** MCP for Unity 通过 [Model Context Protocol](https://modelcontextprotocol.io/introduction) 将 AI 助手(Claude、Claude Code、Codex、Cursor、VS Code 等)与你的 Unity Editor 连接起来。为你的大语言模型提供管理资源、控制场景、编辑脚本和自动化任务的工具。 MCP for Unity building a scene @@ -46,7 +46,7 @@ * **Unity 2021.3 LTS+** — [下载 Unity](https://unity.com/download) * **Python 3.10+** 和 **uv** — [安装 uv](https://docs.astral.sh/uv/getting-started/installation/) -* **一个 MCP 客户端** — [Claude Desktop](https://claude.ai/download) | [Cursor](https://www.cursor.com/en/downloads) | [VS Code Copilot](https://code.visualstudio.com/docs/copilot/overview) | [GitHub Copilot CLI](https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli) | [OpenClaw](https://openclaw.ai) +* **一个 MCP 客户端** — [Claude Desktop](https://claude.ai/download) | [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | [Codex](https://developers.openai.com/codex/mcp) | [Cursor](https://www.cursor.com/en/downloads) | [VS Code Copilot](https://code.visualstudio.com/docs/copilot/overview) | [GitHub Copilot CLI](https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli) | [OpenClaw](https://openclaw.ai) ### 1. 安装 Unity 包 @@ -81,7 +81,7 @@ openupm add com.coplaydev.unity-mcp 2. 点击 **Start Server**(会在 `localhost:8080` 启动 HTTP 服务器) 3. 从下拉菜单选择你的 MCP Client,然后点击 **Configure** 4. 查找 🟢 "Connected ✓" -5. **连接你的客户端:** 一些客户端(Cursor、Antigravity、OpenClaw)需要在设置里启用 MCP 开关或插件。OpenClaw 还需要启用 `openclaw-mcp-bridge` 插件,并会跟随 MCP for Unity 当前选择的传输方式(HTTP 或 stdio);另一些(Claude Desktop、Claude Code)在配置后会自动连接。 +5. **连接你的客户端:** 一些客户端(Cursor、Antigravity、OpenClaw)需要在设置里启用 MCP 开关或插件。OpenClaw 还需要启用 `openclaw-mcp-bridge` 插件,并会跟随 MCP for Unity 当前选择的传输方式(HTTP 或 stdio);另一些(Claude Desktop、Claude Code、Codex)会在配置后或下一个会话中读取该服务器。 **就这些!** 试试这样的提示词:*"Create a red, blue and yellow cube"* 或 *"Build a simple player controller"* @@ -133,6 +133,14 @@ openupm add com.coplaydev.unity-mcp } ``` +**Codex CLI:** +```bash +codex mcp add unityMCP --url http://127.0.0.1:8080/mcp +codex mcp list +``` + +Codex 会把 MCP server 保存到 `~/.codex/config.toml`。如果你使用带 `X-API-Key` 的 remote-hosted 模式,请使用 Unity Editor 的 Configure 按钮,或参考 [CODEX_HELP.md](../guides/CODEX_HELP.md) 中的 TOML 配置。 +
Stdio 配置(uvx) @@ -204,6 +212,7 @@ MCP for Unity 支持多个 Unity Editor 实例。要将操作定向到某个特 **详细的设置指南:** * [Fix Unity MCP and Cursor, VSCode & Windsurf](https://github.com/CoplayDev/unity-mcp/wiki/1.-Fix-Unity-MCP-and-Cursor,-VSCode-&-Windsurf) — uv/Python 安装、PATH 问题 * [Fix Unity MCP and Claude Code](https://github.com/CoplayDev/unity-mcp/wiki/2.-Fix-Unity-MCP-and-Claude-Code) — Claude CLI 安装 +* [Codex setup help](../guides/CODEX_HELP.md) — Codex CLI 注册、TOML fallback、验证步骤 * [Common Setup Problems](https://github.com/CoplayDev/unity-mcp/wiki/3.-Common-Setup-Problems) — macOS dyld 错误、FAQ 还是卡住?[开一个 Issue](https://github.com/CoplayDev/unity-mcp/issues) 或 [加入 Discord](https://discord.gg/y4p8KfzrN4)