diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 8050d9dbd6..194cf085b3 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -12777,6 +12777,7 @@ }, "McpAuthStatus": { "enum": [ + "unknown", "unsupported", "notLoggedIn", "bearerToken", diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index f275f7fc58..2506b97869 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -9119,6 +9119,7 @@ }, "McpAuthStatus": { "enum": [ + "unknown", "unsupported", "notLoggedIn", "bearerToken", diff --git a/codex-rs/app-server-protocol/schema/json/v2/ListMcpServerStatusResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ListMcpServerStatusResponse.json index 0dc2f5e2e4..f30c1cc214 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ListMcpServerStatusResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ListMcpServerStatusResponse.json @@ -3,6 +3,7 @@ "definitions": { "McpAuthStatus": { "enum": [ + "unknown", "unsupported", "notLoggedIn", "bearerToken", diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/McpAuthStatus.ts b/codex-rs/app-server-protocol/schema/typescript/v2/McpAuthStatus.ts index 6903a12321..67d1233dcb 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/McpAuthStatus.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/McpAuthStatus.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type McpAuthStatus = "unsupported" | "notLoggedIn" | "bearerToken" | "oAuth"; +export type McpAuthStatus = "unknown" | "unsupported" | "notLoggedIn" | "bearerToken" | "oAuth"; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs b/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs index 3331646bfd..95317c8d01 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs @@ -16,6 +16,7 @@ use ts_rs::TS; v2_enum_from_core!( pub enum McpAuthStatus from codex_protocol::protocol::McpAuthStatus { + Unknown, Unsupported, NotLoggedIn, BearerToken, diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index 4984ca4788..2bbcde4d6e 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -2409,7 +2409,7 @@ fn mcp_server_status_serializes_absent_server_info_as_null() { tools: HashMap::new(), resources: Vec::new(), resource_templates: Vec::new(), - auth_status: McpAuthStatus::Unsupported, + auth_status: McpAuthStatus::Unknown, }], next_cursor: None, }; @@ -2423,7 +2423,7 @@ fn mcp_server_status_serializes_absent_server_info_as_null() { "tools": {}, "resources": [], "resourceTemplates": [], - "authStatus": "unsupported", + "authStatus": "unknown", }], "nextCursor": null, }) diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index d6002d947d..a79faecddf 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -241,7 +241,7 @@ Example with notification opt-out: - `mcpServer/oauth/login` — start an OAuth login for a configured MCP server; pass `threadId` to resolve servers from that thread's selected plugins and executor, and receive an `authorization_url` followed by `mcpServer/oauthLogin/completed` once the browser flow finishes. - `tool/requestUserInput` — prompt the user with 1–3 short questions for a tool call and return their answers (experimental). - `config/mcpServer/reload` — reload MCP server config from disk and queue a refresh for loaded threads (applied on each thread's next active turn); returns `{}`. Use this after editing `config.toml` without restarting the server. -- `mcpServerStatus/list` — enumerate configured MCP servers with their tools, auth status, server info, plus resources/resource templates for `full` detail; supports optional `threadId` and cursor+limit pagination. If `threadId` is omitted, the server reads from the latest global config directly. If `detail` is omitted, the server defaults to `full`. +- `mcpServerStatus/list` — enumerate configured MCP servers with their tools, auth status, server info, plus resources/resource templates for `full` detail; supports optional `threadId` and cursor+limit pagination. If `threadId` is omitted, the server reads from the latest global config directly. If `detail` is omitted, the server defaults to `full`. An `unknown` auth status means OAuth support could not be determined; `unsupported` means OAuth is known not to be supported. - `mcpServer/resource/read` — read a resource from a configured MCP server by optional `threadId`, `server`, and `uri`, returning text/blob resource `contents`. If `threadId` is omitted, the server reads from the latest MCP config directly. - `mcpServer/tool/call` — call a tool on a thread's configured MCP server by `threadId`, `server`, `tool`, optional `arguments`, and optional `_meta`, returning the MCP tool result. - `windowsSandbox/setupStart` — start Windows sandbox setup for the selected mode (`elevated` or `unelevated`); accepts an optional absolute `cwd` to target setup for a specific workspace, returns `{ started: true }` immediately, and later emits `windowsSandbox/setupCompleted`. diff --git a/codex-rs/cli/tests/mcp_list.rs b/codex-rs/cli/tests/mcp_list.rs index 735ca34b54..40380f482a 100644 --- a/codex-rs/cli/tests/mcp_list.rs +++ b/codex-rs/cli/tests/mcp_list.rs @@ -232,6 +232,36 @@ async fn list_discovers_local_oauth_server_through_environment_proxy() -> Result Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn list_reports_unknown_auth_status_when_oauth_discovery_is_rate_limited() -> Result<()> { + let codex_home = TempDir::new()?; + let server = wiremock::MockServer::start().await; + wiremock::Mock::given(wiremock::matchers::method("GET")) + .and(wiremock::matchers::path("/mcp")) + .respond_with(wiremock::ResponseTemplate::new(429)) + .expect(1) + .mount(&server) + .await; + configure_http_oauth_server(codex_home.path(), &format!("{}/mcp", server.uri())).await?; + + codex_command(codex_home.path())? + .env("NO_PROXY", "127.0.0.1,localhost") + .env("no_proxy", "127.0.0.1,localhost") + .args([ + "-c", + "mcp_oauth_credentials_store=\"file\"", + "mcp", + "list", + "--json", + ]) + .assert() + .success() + .stdout(contains(r#""auth_status": "unknown""#)); + server.verify().await; + + Ok(()) +} + #[cfg(target_os = "macos")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn list_with_macos_proxy_resolution_does_not_panic() -> Result<()> { diff --git a/codex-rs/codex-mcp/src/connection_manager/startup.rs b/codex-rs/codex-mcp/src/connection_manager/startup.rs index ec255e5813..3518975ff0 100644 --- a/codex-rs/codex-mcp/src/connection_manager/startup.rs +++ b/codex-rs/codex-mcp/src/connection_manager/startup.rs @@ -62,6 +62,7 @@ pub(super) fn mcp_startup_failure_reason( } Some( McpAuthState::Unsupported + | McpAuthState::Unknown | McpAuthState::LoggedOut(McpLoginRequirement::Login) | McpAuthState::BearerToken | McpAuthState::OAuth, diff --git a/codex-rs/codex-mcp/src/mcp/auth.rs b/codex-rs/codex-mcp/src/mcp/auth.rs index 1a9863190a..0cb1f867d4 100644 --- a/codex-rs/codex-mcp/src/mcp/auth.rs +++ b/codex-rs/codex-mcp/src/mcp/auth.rs @@ -190,7 +190,7 @@ where Ok(status) => status, Err(error) => { warn!("failed to determine auth status for MCP server `{name}`: {error:?}"); - McpAuthState::Unsupported + McpAuthState::Unknown } }; let entry = McpAuthStatusEntry { diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 2df2081504..f9c6642c8b 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -3770,6 +3770,7 @@ pub struct McpStartupFailure { #[serde(rename_all = "snake_case")] #[ts(rename_all = "snake_case")] pub enum McpAuthStatus { + Unknown, Unsupported, NotLoggedIn, BearerToken, @@ -3779,6 +3780,7 @@ pub enum McpAuthStatus { impl fmt::Display for McpAuthStatus { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let text = match self { + McpAuthStatus::Unknown => "Unknown", McpAuthStatus::Unsupported => "Unsupported", McpAuthStatus::NotLoggedIn => "Not logged in", McpAuthStatus::BearerToken => "Bearer token", diff --git a/codex-rs/rmcp-client/src/auth_status.rs b/codex-rs/rmcp-client/src/auth_status.rs index e82a4fa953..c85b784da4 100644 --- a/codex-rs/rmcp-client/src/auth_status.rs +++ b/codex-rs/rmcp-client/src/auth_status.rs @@ -49,6 +49,7 @@ pub enum McpLoginRequirement { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum McpAuthState { Unsupported, + Unknown, LoggedOut(McpLoginRequirement), BearerToken, OAuth, @@ -58,6 +59,7 @@ impl From for McpAuthStatus { fn from(value: McpAuthState) -> Self { match value { McpAuthState::Unsupported => Self::Unsupported, + McpAuthState::Unknown => Self::Unknown, McpAuthState::LoggedOut(_) => Self::NotLoggedIn, McpAuthState::BearerToken => Self::BearerToken, McpAuthState::OAuth => Self::OAuth, @@ -180,7 +182,7 @@ fn determine_auth_status_from_discovery( debug!( "failed to detect OAuth support for MCP server `{server_name}` at {url}: {error:?}" ); - Ok(McpAuthState::Unsupported) + Err(error) } } } @@ -576,7 +578,7 @@ mod tests { } #[tokio::test] - async fn oauth_discovery_preserves_transient_http_errors() { + async fn determine_auth_status_preserves_transient_http_errors() { for status in [ StatusCode::REQUEST_TIMEOUT, StatusCode::TOO_EARLY, @@ -590,22 +592,26 @@ mod tests { .mount(&server) .await; - let error = discover_streamable_http_oauth( + let error = determine_streamable_http_auth_status( + "transient-http-error", &format!("{}/mcp", server.uri()), + /*bearer_token_env_var*/ None, /*http_headers*/ None, /*env_http_headers*/ None, + OAuthCredentialsStoreMode::File, + AuthKeyringBackendKind::default(), test_http_client(), OAuthDiscoveryTimeout::LOCAL, ) .await - .expect_err("transient OAuth discovery failures must not become anonymous access"); + .expect_err("transient OAuth discovery failures must not become unsupported access"); assert!( matches!( error.downcast_ref::(), Some(AuthError::MetadataError(reason)) if reason.contains(status.as_str()) ), - "OAuth discovery must preserve HTTP {status}: {error:#}" + "auth-status discovery must preserve HTTP {status}: {error:#}" ); server.verify().await; } diff --git a/codex-rs/tui/src/app/background_requests.rs b/codex-rs/tui/src/app/background_requests.rs index c27a5e8ec3..d2b2c075d9 100644 --- a/codex-rs/tui/src/app/background_requests.rs +++ b/codex-rs/tui/src/app/background_requests.rs @@ -1263,6 +1263,7 @@ pub(super) fn mcp_inventory_maps_from_statuses(statuses: Vec) - auth_statuses.insert( server_name.clone(), match status.auth_status { + codex_app_server_protocol::McpAuthStatus::Unknown => McpAuthStatus::Unknown, codex_app_server_protocol::McpAuthStatus::Unsupported => McpAuthStatus::Unsupported, codex_app_server_protocol::McpAuthStatus::NotLoggedIn => McpAuthStatus::NotLoggedIn, codex_app_server_protocol::McpAuthStatus::BearerToken => McpAuthStatus::BearerToken, diff --git a/codex-rs/tui/src/history_cell/mcp.rs b/codex-rs/tui/src/history_cell/mcp.rs index 1843769f0d..27205ca460 100644 --- a/codex-rs/tui/src/history_cell/mcp.rs +++ b/codex-rs/tui/src/history_cell/mcp.rs @@ -16,6 +16,7 @@ impl HistoryCell for McpImageOutputCell { } fn mcp_auth_status_label(status: McpAuthStatus) -> &'static str { match status { + McpAuthStatus::Unknown => "Unknown", McpAuthStatus::Unsupported => "Unsupported", McpAuthStatus::NotLoggedIn => "Not logged in", McpAuthStatus::BearerToken => "Bearer token", @@ -545,6 +546,7 @@ pub(crate) fn new_mcp_tools_output_from_statuses( lines.push(header.into()); let auth_status = match status.auth_status { + codex_app_server_protocol::McpAuthStatus::Unknown => McpAuthStatus::Unknown, codex_app_server_protocol::McpAuthStatus::Unsupported => McpAuthStatus::Unsupported, codex_app_server_protocol::McpAuthStatus::NotLoggedIn => McpAuthStatus::NotLoggedIn, codex_app_server_protocol::McpAuthStatus::BearerToken => McpAuthStatus::BearerToken, diff --git a/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__tests__mcp_tools_output_from_statuses_renders_status_only_servers.snap b/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__tests__mcp_tools_output_from_statuses_renders_status_only_servers.snap index 52bd34c3ac..6fe6aba4a2 100644 --- a/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__tests__mcp_tools_output_from_statuses_renders_status_only_servers.snap +++ b/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__tests__mcp_tools_output_from_statuses_renders_status_only_servers.snap @@ -7,5 +7,5 @@ expression: rendered 🔌 MCP Tools • plugin_docs - • Auth: Unsupported + • Auth: Unknown • Tools: lookup diff --git a/codex-rs/tui/src/history_cell/tests.rs b/codex-rs/tui/src/history_cell/tests.rs index dce26f9507..1a94689e13 100644 --- a/codex-rs/tui/src/history_cell/tests.rs +++ b/codex-rs/tui/src/history_cell/tests.rs @@ -980,7 +980,7 @@ fn mcp_tools_output_from_statuses_renders_status_only_servers() { )]), resources: Vec::new(), resource_templates: Vec::new(), - auth_status: codex_app_server_protocol::McpAuthStatus::Unsupported, + auth_status: codex_app_server_protocol::McpAuthStatus::Unknown, }]; let cell =