Skip to content
Merged
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
1 change: 1 addition & 0 deletions codex-rs/app-server/src/codex_message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4149,6 +4149,7 @@ impl CodexMessageProcessor {
http_headers,
env_http_headers,
scopes.as_deref().unwrap_or_default(),
server.oauth_resource.as_deref(),
timeout_secs,
config.mcp_oauth_callback_port,
config.mcp_oauth_callback_url.as_deref(),
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/cli/src/mcp_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
enabled_tools: None,
disabled_tools: None,
scopes: None,
oauth_resource: None,
};

servers.insert(name.clone(), new_entry);
Expand All @@ -272,6 +273,7 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re
oauth_config.http_headers,
oauth_config.env_http_headers,
&Vec::new(),
None,
config.mcp_oauth_callback_port,
config.mcp_oauth_callback_url.as_deref(),
)
Expand Down Expand Up @@ -356,6 +358,7 @@ async fn run_login(config_overrides: &CliConfigOverrides, login_args: LoginArgs)
http_headers,
env_http_headers,
&scopes,
server.oauth_resource.as_deref(),
config.mcp_oauth_callback_port,
config.mcp_oauth_callback_url.as_deref(),
)
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/core/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1141,6 +1141,10 @@
},
"type": "object"
},
"oauth_resource": {
"default": null,
"type": "string"
},
"required": {
"default": null,
"type": "boolean"
Expand Down
12 changes: 12 additions & 0 deletions codex-rs/core/src/config/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,11 @@ mod document_helpers {
{
entry["scopes"] = array_from_iter(scopes.iter().cloned());
}
if let Some(resource) = &config.oauth_resource
&& !resource.is_empty()
{
entry["oauth_resource"] = value(resource.clone());
}

entry
}
Expand Down Expand Up @@ -1441,6 +1446,7 @@ gpt-5 = "gpt-5.1"
enabled_tools: Some(vec!["one".to_string(), "two".to_string()]),
disabled_tools: None,
scopes: None,
oauth_resource: None,
},
);

Expand All @@ -1465,6 +1471,7 @@ gpt-5 = "gpt-5.1"
enabled_tools: None,
disabled_tools: Some(vec!["forbidden".to_string()]),
scopes: None,
oauth_resource: Some("https://resource.example.com".to_string()),
},
);

Expand All @@ -1483,6 +1490,7 @@ bearer_token_env_var = \"TOKEN\"
enabled = false
startup_timeout_sec = 5.0
disabled_tools = [\"forbidden\"]
oauth_resource = \"https://resource.example.com\"

[mcp_servers.http.http_headers]
Z-Header = \"z\"
Expand Down Expand Up @@ -1532,6 +1540,7 @@ foo = { command = "cmd" }
enabled_tools: None,
disabled_tools: None,
scopes: None,
oauth_resource: None,
},
);

Expand Down Expand Up @@ -1578,6 +1587,7 @@ foo = { command = "cmd" } # keep me
enabled_tools: None,
disabled_tools: None,
scopes: None,
oauth_resource: None,
},
);

Expand Down Expand Up @@ -1623,6 +1633,7 @@ foo = { command = "cmd", args = ["--flag"] } # keep me
enabled_tools: None,
disabled_tools: None,
scopes: None,
oauth_resource: None,
},
);

Expand Down Expand Up @@ -1669,6 +1680,7 @@ foo = { command = "cmd" }
enabled_tools: None,
disabled_tools: None,
scopes: None,
oauth_resource: None,
},
);

Expand Down
60 changes: 60 additions & 0 deletions codex-rs/core/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2411,6 +2411,7 @@ mod tests {
enabled_tools: None,
disabled_tools: None,
scopes: None,
oauth_resource: None,
}
}

Expand All @@ -2430,6 +2431,7 @@ mod tests {
enabled_tools: None,
disabled_tools: None,
scopes: None,
oauth_resource: None,
}
}

Expand Down Expand Up @@ -3464,6 +3466,7 @@ profile = "project"
enabled_tools: None,
disabled_tools: None,
scopes: None,
oauth_resource: None,
},
);

Expand Down Expand Up @@ -3621,6 +3624,7 @@ bearer_token = "secret"
enabled_tools: None,
disabled_tools: None,
scopes: None,
oauth_resource: None,
},
)]);

Expand Down Expand Up @@ -3692,6 +3696,7 @@ ZIG_VAR = "3"
enabled_tools: None,
disabled_tools: None,
scopes: None,
oauth_resource: None,
},
)]);

Expand Down Expand Up @@ -3743,6 +3748,7 @@ ZIG_VAR = "3"
enabled_tools: None,
disabled_tools: None,
scopes: None,
oauth_resource: None,
},
)]);

Expand Down Expand Up @@ -3792,6 +3798,7 @@ ZIG_VAR = "3"
enabled_tools: None,
disabled_tools: None,
scopes: None,
oauth_resource: None,
},
)]);

Expand Down Expand Up @@ -3857,6 +3864,7 @@ startup_timeout_sec = 2.0
enabled_tools: None,
disabled_tools: None,
scopes: None,
oauth_resource: None,
},
)]);
apply_blocking(
Expand Down Expand Up @@ -3934,6 +3942,7 @@ X-Auth = "DOCS_AUTH"
enabled_tools: None,
disabled_tools: None,
scopes: None,
oauth_resource: None,
},
)]);

Expand Down Expand Up @@ -3964,6 +3973,7 @@ X-Auth = "DOCS_AUTH"
enabled_tools: None,
disabled_tools: None,
scopes: None,
oauth_resource: None,
},
);
apply_blocking(
Expand Down Expand Up @@ -4032,6 +4042,7 @@ url = "https://example.com/mcp"
enabled_tools: None,
disabled_tools: None,
scopes: None,
oauth_resource: None,
},
),
(
Expand All @@ -4052,6 +4063,7 @@ url = "https://example.com/mcp"
enabled_tools: None,
disabled_tools: None,
scopes: None,
oauth_resource: None,
},
),
]);
Expand Down Expand Up @@ -4135,6 +4147,7 @@ url = "https://example.com/mcp"
enabled_tools: None,
disabled_tools: None,
scopes: None,
oauth_resource: None,
},
)]);

Expand Down Expand Up @@ -4180,6 +4193,7 @@ url = "https://example.com/mcp"
enabled_tools: None,
disabled_tools: None,
scopes: None,
oauth_resource: None,
},
)]);

Expand Down Expand Up @@ -4225,6 +4239,7 @@ url = "https://example.com/mcp"
enabled_tools: Some(vec!["allowed".to_string()]),
disabled_tools: Some(vec!["blocked".to_string()]),
scopes: None,
oauth_resource: None,
},
)]);

Expand Down Expand Up @@ -4253,6 +4268,51 @@ url = "https://example.com/mcp"
Ok(())
}

#[tokio::test]
async fn replace_mcp_servers_streamable_http_serializes_oauth_resource() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;

let servers = BTreeMap::from([(
"docs".to_string(),
McpServerConfig {
transport: McpServerTransportConfig::StreamableHttp {
url: "https://example.com/mcp".to_string(),
bearer_token_env_var: None,
http_headers: None,
env_http_headers: None,
},
enabled: true,
required: false,
disabled_reason: None,
startup_timeout_sec: None,
tool_timeout_sec: None,
enabled_tools: None,
disabled_tools: None,
scopes: None,
oauth_resource: Some("https://resource.example.com".to_string()),
},
)]);

apply_blocking(
codex_home.path(),
None,
&[ConfigEdit::ReplaceMcpServers(servers.clone())],
)?;

let config_path = codex_home.path().join(CONFIG_TOML_FILE);
let serialized = std::fs::read_to_string(&config_path)?;
assert!(serialized.contains(r#"oauth_resource = "https://resource.example.com""#));

let loaded = load_global_mcp_servers(codex_home.path()).await?;
let docs = loaded.get("docs").expect("docs entry");
assert_eq!(
docs.oauth_resource.as_deref(),
Some("https://resource.example.com")
);

Ok(())
}

#[tokio::test]
async fn set_model_updates_defaults() -> anyhow::Result<()> {
let codex_home = TempDir::new()?;
Expand Down
39 changes: 39 additions & 0 deletions codex-rs/core/src/config/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ pub struct McpServerConfig {
/// Optional OAuth scopes to request during MCP login.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scopes: Option<Vec<String>>,

/// Optional OAuth resource parameter to include during MCP login (RFC 8707).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub oauth_resource: Option<String>,
}

// Raw MCP config shape used for deserialization and JSON Schema generation.
Expand Down Expand Up @@ -142,6 +146,8 @@ pub(crate) struct RawMcpServerConfig {
pub disabled_tools: Option<Vec<String>>,
#[serde(default)]
pub scopes: Option<Vec<String>>,
#[serde(default)]
pub oauth_resource: Option<String>,
}

impl<'de> Deserialize<'de> for McpServerConfig {
Expand All @@ -165,6 +171,7 @@ impl<'de> Deserialize<'de> for McpServerConfig {
let enabled_tools = raw.enabled_tools.clone();
let disabled_tools = raw.disabled_tools.clone();
let scopes = raw.scopes.clone();
let oauth_resource = raw.oauth_resource.clone();

fn throw_if_set<E, T>(transport: &str, field: &str, value: Option<&T>) -> Result<(), E>
where
Expand All @@ -188,6 +195,7 @@ impl<'de> Deserialize<'de> for McpServerConfig {
throw_if_set("stdio", "bearer_token", raw.bearer_token.as_ref())?;
throw_if_set("stdio", "http_headers", raw.http_headers.as_ref())?;
throw_if_set("stdio", "env_http_headers", raw.env_http_headers.as_ref())?;
throw_if_set("stdio", "oauth_resource", raw.oauth_resource.as_ref())?;
McpServerTransportConfig::Stdio {
command,
args: raw.args.clone().unwrap_or_default(),
Expand Down Expand Up @@ -221,6 +229,7 @@ impl<'de> Deserialize<'de> for McpServerConfig {
enabled_tools,
disabled_tools,
scopes,
oauth_resource,
})
}
}
Expand Down Expand Up @@ -1084,6 +1093,22 @@ mod tests {
);
}

#[test]
fn deserialize_streamable_http_server_config_with_oauth_resource() {
let cfg: McpServerConfig = toml::from_str(
r#"
url = "https://example.com/mcp"
oauth_resource = "https://api.example.com"
"#,
)
.expect("should deserialize http config with oauth_resource");

assert_eq!(
cfg.oauth_resource,
Some("https://api.example.com".to_string())
);
}

#[test]
fn deserialize_server_config_with_tool_filters() {
let cfg: McpServerConfig = toml::from_str(
Expand Down Expand Up @@ -1138,6 +1163,20 @@ mod tests {
"#,
)
.expect_err("should reject env_http_headers for stdio transport");

let err = toml::from_str::<McpServerConfig>(
r#"
command = "echo"
oauth_resource = "https://api.example.com"
"#,
)
.expect_err("should reject oauth_resource for stdio transport");

assert!(
err.to_string()
.contains("oauth_resource is not supported for stdio"),
"unexpected error: {err}"
);
}

#[test]
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/src/mcp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ fn codex_apps_mcp_server_config(config: &Config, auth: Option<&CodexAuth>) -> Mc
enabled_tools: None,
disabled_tools: None,
scopes: None,
oauth_resource: None,
}
}

Expand Down
Loading
Loading