[codex] Surface MCP reauthentication-required startup failures (#29877)

## Summary

- distinguish expired, non-refreshable stored MCP OAuth credentials from
first-time missing credentials
- carry a typed `failureReason: "reauthenticationRequired"` on the
existing `mcpServer/startupStatus/updated` notification only when user
action is required
- keep the public MCP auth-status API unchanged and regenerate the
app-server protocol schemas and documentation

## Why

An MCP server with an expired access token and no usable refresh token
currently fails startup without giving clients a reliable, typed
recovery signal.

The existing startup-status notification is the natural place to carry
this state. Its nullable `failureReason` keeps the recovery reason
attached to the failed startup transition without adding a one-off
notification. Internally, Codex distinguishes first-time login from
reauthentication and emits the reason only when the startup error itself
requires authentication.

## User impact

App clients can prompt an existing user to reconnect an MCP server when
automatic recovery is impossible by handling a failed
`mcpServer/startupStatus/updated` notification whose `failureReason` is
`reauthenticationRequired`. Starting, ready, cancelled, unrelated
failures, and first-time setup carry no reauthentication reason.

## Companion app PR

- openai/openai#1069582

## Validation

- `just test -p codex-app-server-protocol` — 248 passed; schema fixture
tests passed
- `cargo check -p codex-app-server -p codex-tui`
- `just test -p codex-rmcp-client -p codex-mcp` — 184 passed, 2 skipped
- `just test -p codex-protocol -p codex-app-server-protocol -p
codex-mcp` — 579 passed
- `just write-app-server-schema`
- `just fmt`
This commit is contained in:
felixxia-oai authored and GitHub committed 2026-06-25 21:50:36 +00:00
1 parent b80fbb70cd
commit a6d20ed297
27 files changed
+350 -55

No files matched your search

@@ -25,7 +25,6 @@ use codex_protocol::ToolName;
use codex_protocol::mcp::McpServerInfo;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::GranularApprovalConfig;
use codex_protocol::protocol::McpAuthStatus;
use codex_rmcp_client::InProcessTransportFactory;
use codex_rmcp_client::RmcpClient;
use futures::FutureExt;
@@ -927,6 +926,7 @@ async fn list_all_tools_uses_shared_codex_apps_cache_when_client_startup_fails()
let failed_client = futures::future::ready::<Result<ManagedClient, StartupOutcomeError>>(Err(
StartupOutcomeError::Failed {
error: "startup failed".to_string(),
is_authentication_required: false,
},
))
.boxed()
@@ -1236,7 +1236,7 @@ fn mcp_init_error_display_prompts_for_github_pat() {
oauth_resource: None,
tools: HashMap::new(),
}),
auth_status: McpAuthStatus::Unsupported,
auth_state: McpAuthState::Unsupported,
};
let err: StartupOutcomeError = anyhow::anyhow!("OAuth is unsupported").into();
@@ -1263,6 +1263,50 @@ fn mcp_init_error_display_prompts_for_login_when_auth_required() {
assert_eq!(expected, display);
}
#[test]
fn mcp_startup_failure_reason_requires_existing_oauth_and_auth_failure() {
for (auth_state, is_authentication_required, expected) in [
(
Some(McpAuthState::LoggedOut(
McpLoginRequirement::Reauthentication,
)),
true,
Some(McpStartupFailureReason::ReauthenticationRequired),
),
(
Some(McpAuthState::LoggedOut(
McpLoginRequirement::Reauthentication,
)),
false,
None,
),
(
Some(McpAuthState::LoggedOut(McpLoginRequirement::Login)),
true,
None,
),
(Some(McpAuthState::Unsupported), true, None),
(Some(McpAuthState::BearerToken), true, None),
(Some(McpAuthState::OAuth), true, None),
(None, true, None),
] {
let entry = auth_state.map(|auth_state| McpAuthStatusEntry {
config: None,
auth_state,
});
let error = StartupOutcomeError::Failed {
error: "startup failed".to_string(),
is_authentication_required,
};
assert_eq!(
mcp_startup_failure_reason(entry.as_ref(), &error),
expected,
"auth_state={auth_state:?}, is_authentication_required={is_authentication_required}"
);
}
}
#[test]
fn mcp_init_error_display_reports_generic_errors() {
let server_name = "custom";
@@ -1290,7 +1334,7 @@ fn mcp_init_error_display_reports_generic_errors() {
oauth_resource: None,
tools: HashMap::new(),
}),
auth_status: McpAuthStatus::Unsupported,
auth_state: McpAuthState::Unsupported,
};
let err: StartupOutcomeError = anyhow::anyhow!("boom").into();