feat(codex): add mcp_config_remote_path for remote MCP server configurations

This commit is contained in:
35C4n0r 2026-05-19 03:54:50 +00:00
parent f9802456ce
commit 853ffb1558
5 changed files with 222 additions and 5 deletions

View File

@ -13,7 +13,7 @@ Install and configure the [Codex CLI](https://github.com/openai/codex) in your w
```tf
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "5.0.0"
version = "5.1.0"
agent_id = coder_agent.main.id
openai_api_key = var.openai_api_key
}
@ -33,7 +33,7 @@ locals {
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "5.0.0"
version = "5.1.0"
agent_id = coder_agent.main.id
workdir = local.codex_workdir
openai_api_key = var.openai_api_key
@ -64,7 +64,7 @@ resource "coder_app" "codex" {
```tf
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "5.0.0"
version = "5.1.0"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
enable_ai_gateway = true
@ -88,7 +88,7 @@ When `enable_ai_gateway = true`, the module configures Codex to use the `aigatew
```tf
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "5.0.0"
version = "5.1.0"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
openai_api_key = var.openai_api_key
@ -107,9 +107,26 @@ module "codex" {
args = ["-y", "@modelcontextprotocol/server-github"]
type = "stdio"
EOT
mcp_config_remote_path = [
"https://example.com/team-mcp-servers.toml",
"https://raw.githubusercontent.com/your-org/your-repo/main/.codex/mcp.toml",
]
}
```
> [!NOTE]
> Servers configured through `mcp` or `mcp_config_remote_path` are appended to `~/.codex/config.toml`, so they apply to every Codex session in the workspace. Each remote URL must return a body in Codex's native TOML format with one or more `[mcp_servers.<name>]` sections; entries that don't contain a `[mcp_servers.*]` are rejected with a warning.
>
> ```toml
> [mcp_servers.my-tool]
> command = "my-tool-server"
> args = ["--port", "8080"]
> type = "stdio"
> ```
>
> A fetch failure (network error, non-2xx response, or invalid body) logs a warning and continues with the remaining URLs.
### Serialize a downstream `coder_script` after the install pipeline
The module exposes the `scripts` output: an ordered list of `coder exp sync` names for the scripts this module creates (pre_install, install, post_install). Scripts that were not configured are absent.
@ -117,7 +134,7 @@ The module exposes the `scripts` output: an ordered list of `coder exp sync` nam
```tf
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "5.0.0"
version = "5.1.0"
agent_id = coder_agent.main.id
openai_api_key = var.openai_api_key
}

View File

@ -425,6 +425,145 @@ describe("codex", async () => {
expect(installLog).toContain("Installed Codex CLI");
});
test("mcp-config-remote-path", async () => {
const remoteToml = [
"[mcp_servers.remote-fetched]",
'command = "remote-mcp-cmd"',
'args = ["--from-url"]',
'type = "stdio"',
].join("\n");
const projectDir = "/home/coder/project";
const moduleDir = path.resolve(import.meta.dir);
const state = await runTerraformApply(moduleDir, {
agent_id: "foo",
workdir: projectDir,
install_codex: "false",
mcp_config_remote_path: JSON.stringify([
"http://localhost:19999/mcp.toml",
"file:///tmp/remote-mcp.toml",
]),
});
const scripts = collectScripts(state);
const coderEnvVars = extractCoderEnvVars(state);
const id = await runContainer("codercom/enterprise-node:latest");
registerCleanup(async () => {
if (process.env["DEBUG"] === "true" || process.env["DEBUG"] === "1") {
console.log(`Not removing container ${id} in debug mode`);
return;
}
await removeContainer(id);
});
await execContainer(id, ["bash", "-c", `mkdir -p '${projectDir}'`]);
await writeExecutable({
containerId: id,
filePath: "/usr/bin/coder",
content: "#!/bin/bash\nexit 0\n",
});
await writeExecutable({
containerId: id,
filePath: "/usr/bin/codex",
content: await Bun.file(
path.join(moduleDir, "testdata", "codex-mock.sh"),
).text(),
});
// Drop the remote TOML payload at a path the install script will fetch
// via file:// — keeps the test self-contained (no external network).
await execContainer(id, [
"bash",
"-c",
`cat > /tmp/remote-mcp.toml <<'EOF'\n${remoteToml}\nEOF`,
]);
await runScripts(id, scripts, coderEnvVars);
const installLog = await readFileContainer(
id,
"/home/coder/.coder-modules/coder-labs/codex/logs/install.log",
);
// Both URLs were attempted.
expect(installLog).toContain("http://localhost:19999/mcp.toml");
expect(installLog).toContain("file:///tmp/remote-mcp.toml");
// First URL fails gracefully.
expect(installLog).toContain(
"Warning: Failed to fetch MCP configuration from 'http://localhost:19999/mcp.toml'",
);
// Second URL succeeds.
expect(installLog).not.toContain(
"Warning: Failed to fetch MCP configuration from 'file:///tmp/remote-mcp.toml'",
);
expect(installLog).toContain(
"Appending MCP servers from file:///tmp/remote-mcp.toml",
);
const configToml = await readFileContainer(
id,
"/home/coder/.codex/config.toml",
);
expect(configToml).toContain("[mcp_servers.remote-fetched]");
expect(configToml).toContain('command = "remote-mcp-cmd"');
});
test("mcp-config-remote-path-invalid-toml", async () => {
const projectDir = "/home/coder/project";
const moduleDir = path.resolve(import.meta.dir);
const state = await runTerraformApply(moduleDir, {
agent_id: "foo",
workdir: projectDir,
install_codex: "false",
mcp_config_remote_path: JSON.stringify(["file:///tmp/invalid-mcp.toml"]),
});
const scripts = collectScripts(state);
const coderEnvVars = extractCoderEnvVars(state);
const id = await runContainer("codercom/enterprise-node:latest");
registerCleanup(async () => {
if (process.env["DEBUG"] === "true" || process.env["DEBUG"] === "1") {
console.log(`Not removing container ${id} in debug mode`);
return;
}
await removeContainer(id);
});
await execContainer(id, ["bash", "-c", `mkdir -p '${projectDir}'`]);
await writeExecutable({
containerId: id,
filePath: "/usr/bin/coder",
content: "#!/bin/bash\nexit 0\n",
});
await writeExecutable({
containerId: id,
filePath: "/usr/bin/codex",
content: await Bun.file(
path.join(moduleDir, "testdata", "codex-mock.sh"),
).text(),
});
// Fetched body has no [mcp_servers.*] section — the install script should
// reject it rather than appending random content to config.toml.
await execContainer(id, [
"bash",
"-c",
`cat > /tmp/invalid-mcp.toml <<'EOF'\nnot_a_valid_mcp_section = true\nEOF`,
]);
await runScripts(id, scripts, coderEnvVars);
const installLog = await readFileContainer(
id,
"/home/coder/.coder-modules/coder-labs/codex/logs/install.log",
);
expect(installLog).toContain(
"Warning: Invalid MCP configuration from 'file:///tmp/invalid-mcp.toml'",
);
const configToml = await readFileContainer(
id,
"/home/coder/.codex/config.toml",
);
expect(configToml).not.toContain("not_a_valid_mcp_section");
});
test("custom-config-drops-reasoning-effort", async () => {
const baseConfig = [
'sandbox_mode = "danger-full-access"',

View File

@ -88,6 +88,12 @@ variable "mcp" {
default = ""
}
variable "mcp_config_remote_path" {
type = list(string)
description = "List of URLs that return MCP server configurations in TOML format (matching Codex's native config format). Fetched at install time and appended to config.toml."
default = []
}
variable "model_reasoning_effort" {
type = string
description = "The reasoning effort for the model. One of: none, minimal, low, medium, high, xhigh. See https://platform.openai.com/docs/guides/latest-model#lower-reasoning-effort"
@ -141,6 +147,7 @@ locals {
ARG_WORKDIR = local.workdir != "" ? base64encode(local.workdir) : ""
ARG_BASE_CONFIG_TOML = var.base_config_toml != "" ? base64encode(var.base_config_toml) : ""
ARG_MCP = var.mcp != "" ? base64encode(var.mcp) : ""
ARG_MCP_CONFIG_REMOTE_PATH = base64encode(jsonencode(var.mcp_config_remote_path))
ARG_ENABLE_AI_GATEWAY = tostring(var.enable_ai_gateway)
ARG_AIBRIDGE_CONFIG = var.enable_ai_gateway ? base64encode(local.aibridge_config) : ""
ARG_MODEL_REASONING_EFFORT = var.model_reasoning_effort

View File

@ -183,3 +183,39 @@ run "test_workdir_optional" {
error_message = "scripts output should have install script even without workdir"
}
}
run "test_mcp_config_remote_path" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
mcp_config_remote_path = [
"https://example.com/mcp-one.toml",
"https://example.com/mcp-two.toml",
]
}
assert {
condition = length(var.mcp_config_remote_path) == 2
error_message = "mcp_config_remote_path should accept a list of URLs"
}
assert {
condition = strcontains(local.install_script, base64encode(jsonencode(var.mcp_config_remote_path)))
error_message = "install script should embed the base64-encoded mcp_config_remote_path JSON"
}
}
run "test_mcp_config_remote_path_default" {
command = plan
variables {
agent_id = "test-agent"
}
assert {
condition = length(var.mcp_config_remote_path) == 0
error_message = "mcp_config_remote_path should default to an empty list"
}
}

View File

@ -13,6 +13,7 @@ ARG_CODEX_VERSION=$(echo -n '${ARG_CODEX_VERSION}' | base64 -d)
ARG_WORKDIR=$(echo -n '${ARG_WORKDIR}' | base64 -d)
ARG_BASE_CONFIG_TOML=$(echo -n '${ARG_BASE_CONFIG_TOML}' | base64 -d)
ARG_MCP=$(echo -n '${ARG_MCP}' | base64 -d)
ARG_MCP_CONFIG_REMOTE_PATH=$(echo -n '${ARG_MCP_CONFIG_REMOTE_PATH}' | base64 -d)
ARG_ENABLE_AI_GATEWAY='${ARG_ENABLE_AI_GATEWAY}'
ARG_AIBRIDGE_CONFIG=$(echo -n '${ARG_AIBRIDGE_CONFIG}' | base64 -d)
ARG_MODEL_REASONING_EFFORT='${ARG_MODEL_REASONING_EFFORT}'
@ -24,6 +25,7 @@ printf "workdir: %s\n" "$${ARG_WORKDIR}"
printf "enable_ai_gateway: %s\n" "$${ARG_ENABLE_AI_GATEWAY}"
printf "install_codex: %s\n" "$${ARG_INSTALL}"
printf "model_reasoning_effort: %s\n" "$${ARG_MODEL_REASONING_EFFORT}"
printf "mcp_config_remote_path: %s\n" "$${ARG_MCP_CONFIG_REMOTE_PATH}"
echo "--------------------------------"
function add_path_to_shell_profiles() {
@ -155,6 +157,22 @@ function populate_config_toml() {
echo "$${ARG_MCP}" >> "$${config_path}"
fi
if [ -n "$${ARG_MCP_CONFIG_REMOTE_PATH}" ] && [ "$${ARG_MCP_CONFIG_REMOTE_PATH}" != "[]" ]; then
for url in $(echo "$${ARG_MCP_CONFIG_REMOTE_PATH}" | jq -r '.[]'); do
echo "Fetching MCP configuration from: $${url}"
mcp_toml=$(curl -fsSL "$${url}") || {
echo "Warning: Failed to fetch MCP configuration from '$${url}', continuing..."
continue
}
if ! echo "$${mcp_toml}" | grep -qE '^\[mcp_servers\.'; then
echo "Warning: Invalid MCP configuration from '$${url}' (missing [mcp_servers.*] section), continuing..."
continue
fi
printf "Appending MCP servers from %s\n" "$${url}"
printf '\n%s\n' "$${mcp_toml}" >> "$${config_path}"
done
fi
if [ "$${ARG_ENABLE_AI_GATEWAY}" = "true" ] && [ -n "$${ARG_AIBRIDGE_CONFIG}" ]; then
if ! grep -q '\[model_providers\.aigateway\]' "$${config_path}" 2>/dev/null; then
printf "Adding AI Gateway configuration\n"