diff --git a/modules/programs/codex.nix b/modules/programs/codex.nix index d931dcc5..68be40b7 100644 --- a/modules/programs/codex.nix +++ b/modules/programs/codex.nix @@ -26,6 +26,20 @@ in package = lib.mkPackageOption pkgs "codex" { nullable = true; }; + enableMcpIntegration = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Whether to integrate the MCP server config from + {option}`programs.mcp.servers` into + {option}`programs.codex.settings.mcp_servers`. + + Note: Settings defined in {option}`programs.mcp.servers` are merged + with {option}`programs.codex.settings.mcp_servers`, with settings-based + values taking precedence. + ''; + }; + settings = lib.mkOption { # NOTE: `yaml` type supports null, using `nullOr` for backwards compatibility period type = lib.types.nullOr tomlFormat.type; @@ -48,6 +62,15 @@ in envKey = "OLLAMA_API_KEY"; }; }; + mcp_servers = { + context7 = { + command = "npx"; + args = [ + "-y" + "@upstash/context7-mcp" + ]; + }; + }; } ''; }; @@ -111,10 +134,36 @@ in config = let - useXdgDirectories = (config.home.preferXdgDirectories && isTomlConfig); + useXdgDirectories = config.home.preferXdgDirectories && isTomlConfig; xdgConfigHome = lib.removePrefix config.home.homeDirectory config.xdg.configHome; configDir = if useXdgDirectories then "${xdgConfigHome}/codex" else ".codex"; configFileName = if isTomlConfig then "config.toml" else "config.yaml"; + + transformedMcpServers = lib.optionalAttrs (cfg.enableMcpIntegration && config.programs.mcp.enable) ( + lib.mapAttrs ( + _name: server: + # NOTE: Convert shared programs.mcp fields to Codex config keys: + # - removeAttrs drops keys that Codex does not use directly + # - "disabled" becomes inverse "enabled" + # - "headers" is renamed to "http_headers" + # See: https://developers.openai.com/codex/mcp#other-configuration-options + (lib.removeAttrs server [ + "disabled" + "headers" + ]) + // (lib.optionalAttrs (server ? headers && !(server ? http_headers)) { + http_headers = server.headers; + }) + // { + enabled = !(server.disabled or false); + } + ) config.programs.mcp.servers + ); + + settingMcpServers = lib.attrByPath [ "mcp_servers" ] { } cfg.settings; + mergedMcpServers = transformedMcpServers // settingMcpServers; + mergedSettings = + cfg.settings // lib.optionalAttrs (mergedMcpServers != { }) { mcp_servers = mergedMcpServers; }; in mkIf cfg.enable { assertions = [ @@ -126,9 +175,10 @@ in home = { packages = mkIf (cfg.package != null) [ cfg.package ]; + file = { - "${configDir}/${configFileName}" = lib.mkIf (cfg.settings != { }) { - source = settingsFormat.generate "codex-config" cfg.settings; + "${configDir}/${configFileName}" = lib.mkIf (mergedSettings != { }) { + source = settingsFormat.generate "codex-config" mergedSettings; }; "${configDir}/AGENTS.md" = lib.mkIf (cfg.custom-instructions != "") { text = cfg.custom-instructions; @@ -150,6 +200,7 @@ in if lib.isPath content then { source = content; } else { text = content; } ) ) (if builtins.isAttrs cfg.skills then cfg.skills else { })); + sessionVariables = mkIf useXdgDirectories { CODEX_HOME = "${config.xdg.configHome}/codex"; }; diff --git a/tests/modules/programs/codex/default.nix b/tests/modules/programs/codex/default.nix index 23eb27b1..6341172a 100644 --- a/tests/modules/programs/codex/default.nix +++ b/tests/modules/programs/codex/default.nix @@ -6,6 +6,8 @@ codex-custom-instructions = ./custom-instructions.nix; codex-custom-instructions-prefer-xdg-directories = ./custom-instructions-prefer-xdg-directories.nix; codex-empty-custom-instructions = ./empty-custom-instructions.nix; + codex-mcp-integration = ./mcp-integration.nix; + codex-mcp-integration-with-override = ./mcp-integration-with-override.nix; codex-skills-inline = ./skills-inline.nix; codex-skills-dir = ./skills-dir.nix; codex-skills-path-not-directory = ./skills-path-not-directory.nix; diff --git a/tests/modules/programs/codex/mcp-integration-with-override.nix b/tests/modules/programs/codex/mcp-integration-with-override.nix new file mode 100644 index 00000000..6907e66b --- /dev/null +++ b/tests/modules/programs/codex/mcp-integration-with-override.nix @@ -0,0 +1,49 @@ +{ + programs.mcp = { + enable = true; + servers = { + everything = { + command = "npx"; + args = [ + "-y" + "@modelcontextprotocol/server-everything" + ]; + }; + context7 = { + url = "https://mcp.context7.com/mcp"; + headers = { + CONTEXT7_API_KEY = "{env:CONTEXT7_API_KEY}"; + }; + }; + }; + }; + + programs.codex = { + enable = true; + enableMcpIntegration = true; + settings = { + model = "gpt-5-codex"; + mcp_servers = { + custom-server = { + url = "http://localhost:3000/mcp"; + enabled = true; + enabled_tools = [ + "open" + "screenshot" + ]; + }; + everything = { + command = "final-command"; + enabled = false; + tool_timeout_sec = 45; + }; + }; + }; + }; + + nmt.script = '' + assertFileExists home-files/.codex/config.toml + assertFileContent home-files/.codex/config.toml \ + ${./mcp-integration-with-override.toml} + ''; +} diff --git a/tests/modules/programs/codex/mcp-integration-with-override.toml b/tests/modules/programs/codex/mcp-integration-with-override.toml new file mode 100644 index 00000000..bf046589 --- /dev/null +++ b/tests/modules/programs/codex/mcp-integration-with-override.toml @@ -0,0 +1,18 @@ +model = "gpt-5-codex" + +[mcp_servers.context7] +enabled = true +url = "https://mcp.context7.com/mcp" + +[mcp_servers.context7.http_headers] +CONTEXT7_API_KEY = "{env:CONTEXT7_API_KEY}" + +[mcp_servers.custom-server] +enabled = true +enabled_tools = ["open", "screenshot"] +url = "http://localhost:3000/mcp" + +[mcp_servers.everything] +command = "final-command" +enabled = false +tool_timeout_sec = 45 diff --git a/tests/modules/programs/codex/mcp-integration.nix b/tests/modules/programs/codex/mcp-integration.nix new file mode 100644 index 00000000..e7b87cf1 --- /dev/null +++ b/tests/modules/programs/codex/mcp-integration.nix @@ -0,0 +1,36 @@ +{ + programs.mcp = { + enable = true; + servers = { + everything = { + command = "npx"; + args = [ + "-y" + "@modelcontextprotocol/server-everything" + ]; + }; + context7 = { + url = "https://mcp.context7.com/mcp"; + headers = { + CONTEXT7_API_KEY = "{env:CONTEXT7_API_KEY}"; + }; + }; + disabled-server = { + command = "echo"; + args = [ "test" ]; + disabled = true; + }; + }; + }; + + programs.codex = { + enable = true; + enableMcpIntegration = true; + }; + + nmt.script = '' + assertFileExists home-files/.codex/config.toml + assertFileContent home-files/.codex/config.toml \ + ${./mcp-integration.toml} + ''; +} diff --git a/tests/modules/programs/codex/mcp-integration.toml b/tests/modules/programs/codex/mcp-integration.toml new file mode 100644 index 00000000..c46f5acd --- /dev/null +++ b/tests/modules/programs/codex/mcp-integration.toml @@ -0,0 +1,16 @@ +[mcp_servers.context7] +enabled = true +url = "https://mcp.context7.com/mcp" + +[mcp_servers.context7.http_headers] +CONTEXT7_API_KEY = "{env:CONTEXT7_API_KEY}" + +[mcp_servers.disabled-server] +args = ["test"] +command = "echo" +enabled = false + +[mcp_servers.everything] +args = ["-y", "@modelcontextprotocol/server-everything"] +command = "npx" +enabled = true