Skip to content
Closed
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
6 changes: 5 additions & 1 deletion codex-rs/codex-mcp/src/connection_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ use codex_protocol::protocol::McpStartupFailure;
use codex_protocol::protocol::McpStartupStatus;
use codex_protocol::protocol::McpStartupUpdateEvent;
use codex_rmcp_client::ElicitationResponse;
use codex_rmcp_client::OAUTH_REFRESH_REAUTHENTICATION_REQUIRED_ERROR;
use rmcp::model::ElicitationCapability;
use rmcp::model::ListResourceTemplatesResult;
use rmcp::model::ListResourcesResult;
Expand Down Expand Up @@ -860,7 +861,10 @@ fn startup_outcome_error_message(error: StartupOutcomeError) -> String {

fn is_mcp_client_auth_required_error(error: &StartupOutcomeError) -> bool {
match error {
StartupOutcomeError::Failed { error } => error.contains("Auth required"),
StartupOutcomeError::Failed { error } => {
error.contains("Auth required")
|| error.contains(OAUTH_REFRESH_REAUTHENTICATION_REQUIRED_ERROR)
}
_ => false,
}
}
Expand Down
64 changes: 63 additions & 1 deletion codex-rs/codex-mcp/src/connection_manager_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use codex_protocol::mcp::McpServerInfo;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::GranularApprovalConfig;
use codex_protocol::protocol::McpAuthStatus;
use codex_rmcp_client::OAUTH_REFRESH_REAUTHENTICATION_REQUIRED_ERROR;
use futures::FutureExt;
use pretty_assertions::assert_eq;
use rmcp::model::CreateElicitationRequestParams;
Expand Down Expand Up @@ -1279,7 +1280,9 @@ fn mcp_init_error_display_prompts_for_github_pat() {
#[test]
fn mcp_init_error_display_prompts_for_login_when_auth_required() {
let server_name = "example";
let err: StartupOutcomeError = anyhow::anyhow!("Auth required for server").into();
let err: StartupOutcomeError =
anyhow::anyhow!("handshaking with MCP server failed: transport error: Auth required")
.into();

let display = mcp_init_error_display(server_name, /*entry*/ None, &err);

Expand All @@ -1290,6 +1293,65 @@ fn mcp_init_error_display_prompts_for_login_when_auth_required() {
assert_eq!(expected, display);
}

#[test]
fn mcp_init_error_display_prompts_for_login_when_refresh_token_is_rejected() {
let server_name = "example";
let err: StartupOutcomeError =
anyhow::anyhow!(OAUTH_REFRESH_REAUTHENTICATION_REQUIRED_ERROR).into();

let display = mcp_init_error_display(server_name, /*entry*/ None, &err);

let expected = format!(
"The {server_name} MCP server is not logged in. Run `codex mcp login {server_name}`."
);
assert_eq!(expected, display);
}

#[test]
fn mcp_init_error_display_keeps_transient_oauth_refresh_failure_generic() {
let server_name = "example";
let err: StartupOutcomeError =
anyhow::anyhow!("OAuth token endpoint refresh failed: 503 Service Unavailable").into();

let display = mcp_init_error_display(server_name, /*entry*/ None, &err);

let expected = format!("MCP client for `{server_name}` failed to start: {err:#}");
assert_eq!(expected, display);
assert!(!display.contains("codex mcp login"));
}

#[test]
fn mcp_init_error_display_preserves_credential_store_read_cause() {
let server_name = "example";
let err: StartupOutcomeError = anyhow::anyhow!(
"failed to read OAuth credentials for MCP server `example`: failed to read credentials file at /tmp/.credentials.json: Is a directory"
)
.into();

let display = mcp_init_error_display(server_name, /*entry*/ None, &err);

let expected = format!("MCP client for `{server_name}` failed to start: {err:#}");
assert_eq!(expected, display);
assert!(display.contains("Is a directory"));
assert!(!display.contains("codex mcp login"));
}

#[test]
fn mcp_init_error_display_preserves_credential_store_write_cause() {
let server_name = "example";
let err: StartupOutcomeError = anyhow::anyhow!(
"failed to write credentials file at /tmp/.credentials.json: Permission denied"
)
.into();

let display = mcp_init_error_display(server_name, /*entry*/ None, &err);

let expected = format!("MCP client for `{server_name}` failed to start: {err:#}");
assert_eq!(expected, display);
assert!(display.contains("Permission denied"));
assert!(!display.contains("codex mcp login"));
}

#[test]
fn mcp_init_error_display_reports_generic_errors() {
let server_name = "custom";
Expand Down
1 change: 1 addition & 0 deletions codex-rs/rmcp-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub use auth_status::discover_streamable_http_oauth;
pub use auth_status::supports_oauth_login;
pub use codex_protocol::protocol::McpAuthStatus;
pub use in_process_transport::InProcessTransportFactory;
pub use oauth::OAUTH_REFRESH_REAUTHENTICATION_REQUIRED_ERROR;
pub use oauth::StoredOAuthTokens;
pub use oauth::WrappedOAuthTokenResponse;
pub use oauth::delete_oauth_tokens;
Expand Down
Loading
Loading