diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 42a90d2e9..fec9b2abd 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -2865,6 +2865,8 @@ impl CodexMessageProcessor { } }; + let scopes = scopes.or_else(|| server.scopes.clone()); + match perform_oauth_login_return_url( &name, &url, diff --git a/codex-rs/cli/src/mcp_cmd.rs b/codex-rs/cli/src/mcp_cmd.rs index 22cd18dde..7cc42c4c4 100644 --- a/codex-rs/cli/src/mcp_cmd.rs +++ b/codex-rs/cli/src/mcp_cmd.rs @@ -247,6 +247,7 @@ async fn run_add(config_overrides: &CliConfigOverrides, add_args: AddArgs) -> Re tool_timeout_sec: None, enabled_tools: None, disabled_tools: None, + scopes: None, }; servers.insert(name.clone(), new_entry); @@ -348,6 +349,11 @@ async fn run_login(config_overrides: &CliConfigOverrides, login_args: LoginArgs) _ => bail!("OAuth login is only supported for streamable HTTP servers."), }; + let mut scopes = scopes; + if scopes.is_empty() { + scopes = server.scopes.clone().unwrap_or_default(); + } + perform_oauth_login( &name, &url, diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 68b9ba699..36d8f676c 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -750,6 +750,13 @@ }, "type": "object" }, + "scopes": { + "default": null, + "items": { + "type": "string" + }, + "type": "array" + }, "startup_timeout_ms": { "default": null, "format": "uint64", diff --git a/codex-rs/core/src/config/edit.rs b/codex-rs/core/src/config/edit.rs index b99fde92b..a34e68934 100644 --- a/codex-rs/core/src/config/edit.rs +++ b/codex-rs/core/src/config/edit.rs @@ -167,6 +167,11 @@ mod document_helpers { { entry["disabled_tools"] = array_from_iter(disabled_tools.iter().cloned()); } + if let Some(scopes) = &config.scopes + && !scopes.is_empty() + { + entry["scopes"] = array_from_iter(scopes.iter().cloned()); + } entry } @@ -1373,6 +1378,7 @@ gpt-5 = "gpt-5.1" tool_timeout_sec: None, enabled_tools: Some(vec!["one".to_string(), "two".to_string()]), disabled_tools: None, + scopes: None, }, ); @@ -1395,6 +1401,7 @@ gpt-5 = "gpt-5.1" tool_timeout_sec: None, enabled_tools: None, disabled_tools: Some(vec!["forbidden".to_string()]), + scopes: None, }, ); @@ -1460,6 +1467,7 @@ foo = { command = "cmd" } tool_timeout_sec: None, enabled_tools: None, disabled_tools: None, + scopes: None, }, ); @@ -1504,6 +1512,7 @@ foo = { command = "cmd" } # keep me tool_timeout_sec: None, enabled_tools: None, disabled_tools: None, + scopes: None, }, ); @@ -1547,6 +1556,7 @@ foo = { command = "cmd", args = ["--flag"] } # keep me tool_timeout_sec: None, enabled_tools: None, disabled_tools: None, + scopes: None, }, ); @@ -1591,6 +1601,7 @@ foo = { command = "cmd" } tool_timeout_sec: None, enabled_tools: None, disabled_tools: None, + scopes: None, }, ); diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index ca3ccc1ff..031f3c6f3 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -1772,6 +1772,7 @@ mod tests { tool_timeout_sec: None, enabled_tools: None, disabled_tools: None, + scopes: None, } } @@ -1789,6 +1790,7 @@ mod tests { tool_timeout_sec: None, enabled_tools: None, disabled_tools: None, + scopes: None, } } @@ -2614,6 +2616,7 @@ profile = "project" tool_timeout_sec: Some(Duration::from_secs(5)), enabled_tools: None, disabled_tools: None, + scopes: None, }, ); @@ -2768,6 +2771,7 @@ bearer_token = "secret" tool_timeout_sec: None, enabled_tools: None, disabled_tools: None, + scopes: None, }, )]); @@ -2837,6 +2841,7 @@ ZIG_VAR = "3" tool_timeout_sec: None, enabled_tools: None, disabled_tools: None, + scopes: None, }, )]); @@ -2886,6 +2891,7 @@ ZIG_VAR = "3" tool_timeout_sec: None, enabled_tools: None, disabled_tools: None, + scopes: None, }, )]); @@ -2933,6 +2939,7 @@ ZIG_VAR = "3" tool_timeout_sec: None, enabled_tools: None, disabled_tools: None, + scopes: None, }, )]); @@ -2996,6 +3003,7 @@ startup_timeout_sec = 2.0 tool_timeout_sec: None, enabled_tools: None, disabled_tools: None, + scopes: None, }, )]); apply_blocking( @@ -3071,6 +3079,7 @@ X-Auth = "DOCS_AUTH" tool_timeout_sec: None, enabled_tools: None, disabled_tools: None, + scopes: None, }, )]); @@ -3099,6 +3108,7 @@ X-Auth = "DOCS_AUTH" tool_timeout_sec: None, enabled_tools: None, disabled_tools: None, + scopes: None, }, ); apply_blocking( @@ -3165,6 +3175,7 @@ url = "https://example.com/mcp" tool_timeout_sec: None, enabled_tools: None, disabled_tools: None, + scopes: None, }, ), ( @@ -3183,6 +3194,7 @@ url = "https://example.com/mcp" tool_timeout_sec: None, enabled_tools: None, disabled_tools: None, + scopes: None, }, ), ]); @@ -3264,6 +3276,7 @@ url = "https://example.com/mcp" tool_timeout_sec: None, enabled_tools: None, disabled_tools: None, + scopes: None, }, )]); @@ -3307,6 +3320,7 @@ url = "https://example.com/mcp" tool_timeout_sec: None, enabled_tools: Some(vec!["allowed".to_string()]), disabled_tools: Some(vec!["blocked".to_string()]), + scopes: None, }, )]); diff --git a/codex-rs/core/src/config/types.rs b/codex-rs/core/src/config/types.rs index 1b3f381d3..96379dfa0 100644 --- a/codex-rs/core/src/config/types.rs +++ b/codex-rs/core/src/config/types.rs @@ -73,6 +73,10 @@ pub struct McpServerConfig { /// Explicit deny-list of tools. These tools will be removed after applying `enabled_tools`. #[serde(default, skip_serializing_if = "Option::is_none")] pub disabled_tools: Option>, + + /// Optional OAuth scopes to request during MCP login. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub scopes: Option>, } // Raw MCP config shape used for deserialization and JSON Schema generation. @@ -113,6 +117,8 @@ pub(crate) struct RawMcpServerConfig { pub enabled_tools: Option>, #[serde(default)] pub disabled_tools: Option>, + #[serde(default)] + pub scopes: Option>, } impl<'de> Deserialize<'de> for McpServerConfig { @@ -134,6 +140,7 @@ impl<'de> Deserialize<'de> for McpServerConfig { let enabled = raw.enabled.unwrap_or_else(default_enabled); let enabled_tools = raw.enabled_tools.clone(); let disabled_tools = raw.disabled_tools.clone(); + let scopes = raw.scopes.clone(); fn throw_if_set(transport: &str, field: &str, value: Option<&T>) -> Result<(), E> where @@ -188,6 +195,7 @@ impl<'de> Deserialize<'de> for McpServerConfig { disabled_reason: None, enabled_tools, disabled_tools, + scopes, }) } } diff --git a/codex-rs/core/src/mcp_connection_manager.rs b/codex-rs/core/src/mcp_connection_manager.rs index 434db3b2f..9462f745b 100644 --- a/codex-rs/core/src/mcp_connection_manager.rs +++ b/codex-rs/core/src/mcp_connection_manager.rs @@ -1182,6 +1182,7 @@ mod tests { tool_timeout_sec: None, enabled_tools: None, disabled_tools: None, + scopes: None, }, auth_status: McpAuthStatus::Unsupported, }; @@ -1227,6 +1228,7 @@ mod tests { tool_timeout_sec: None, enabled_tools: None, disabled_tools: None, + scopes: None, }, auth_status: McpAuthStatus::Unsupported, }; diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index 4210a5c6b..7eaeef659 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -93,6 +93,7 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> { tool_timeout_sec: None, enabled_tools: None, disabled_tools: None, + scopes: None, }, ); config @@ -233,6 +234,7 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> { tool_timeout_sec: None, enabled_tools: None, disabled_tools: None, + scopes: None, }, ); config @@ -431,6 +433,7 @@ async fn stdio_image_completions_round_trip() -> anyhow::Result<()> { tool_timeout_sec: None, enabled_tools: None, disabled_tools: None, + scopes: None, }, ); config @@ -577,6 +580,7 @@ async fn stdio_server_propagates_whitelisted_env_vars() -> anyhow::Result<()> { tool_timeout_sec: None, enabled_tools: None, disabled_tools: None, + scopes: None, }, ); config @@ -734,6 +738,7 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> { tool_timeout_sec: None, enabled_tools: None, disabled_tools: None, + scopes: None, }, ); config @@ -923,6 +928,7 @@ async fn streamable_http_with_oauth_round_trip() -> anyhow::Result<()> { tool_timeout_sec: None, enabled_tools: None, disabled_tools: None, + scopes: None, }, ); config diff --git a/codex-rs/core/tests/suite/truncation.rs b/codex-rs/core/tests/suite/truncation.rs index cf03e0992..c24e263f9 100644 --- a/codex-rs/core/tests/suite/truncation.rs +++ b/codex-rs/core/tests/suite/truncation.rs @@ -431,6 +431,7 @@ async fn mcp_tool_call_output_exceeds_limit_truncated_for_model() -> Result<()> tool_timeout_sec: None, enabled_tools: None, disabled_tools: None, + scopes: None, }, ); config @@ -523,6 +524,7 @@ async fn mcp_image_output_preserves_image_and_no_text_summary() -> Result<()> { tool_timeout_sec: None, enabled_tools: None, disabled_tools: None, + scopes: None, }, ); config @@ -786,6 +788,7 @@ async fn mcp_tool_call_output_not_truncated_with_custom_limit() -> Result<()> { tool_timeout_sec: None, enabled_tools: None, disabled_tools: None, + scopes: None, }, ); config diff --git a/codex-rs/tui/src/history_cell.rs b/codex-rs/tui/src/history_cell.rs index 174f2a7f6..94da257d9 100644 --- a/codex-rs/tui/src/history_cell.rs +++ b/codex-rs/tui/src/history_cell.rs @@ -1953,6 +1953,7 @@ mod tests { tool_timeout_sec: None, enabled_tools: None, disabled_tools: None, + scopes: None, }; let mut servers = config.mcp_servers.get().clone(); servers.insert("docs".to_string(), stdio_config); @@ -1974,6 +1975,7 @@ mod tests { tool_timeout_sec: None, enabled_tools: None, disabled_tools: None, + scopes: None, }; servers.insert("http".to_string(), http_config); config