[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

+10 -13
View File
@@ -9,7 +9,7 @@ use codex_config::types::AuthKeyringBackendKind;
use codex_config::types::OAuthCredentialsStoreMode;
use codex_exec_server::HttpClient;
use codex_login::CodexAuth;
use codex_protocol::protocol::McpAuthStatus;
use codex_rmcp_client::McpAuthState;
use codex_rmcp_client::OAuthProviderError;
use codex_rmcp_client::determine_streamable_http_auth_status;
use codex_rmcp_client::determine_streamable_http_auth_status_with_http_client;
@@ -54,7 +54,7 @@ pub struct ResolvedMcpOAuthScopes {
#[derive(Debug, Clone)]
pub struct McpAuthStatusEntry {
pub config: Option<McpServerConfig>,
pub auth_status: McpAuthStatus,
pub auth_state: McpAuthState,
}
pub async fn oauth_login_support(transport: &McpServerTransportConfig) -> McpOAuthLoginSupport {
@@ -208,7 +208,7 @@ where
)
});
async move {
let auth_status = match config.as_ref() {
let auth_state = match config.as_ref() {
Some(config) => {
match compute_auth_status(
&name,
@@ -225,16 +225,13 @@ where
warn!(
"failed to determine auth status for MCP server `{name}`: {error:?}"
);
McpAuthStatus::Unsupported
McpAuthState::Unsupported
}
}
}
None => McpAuthStatus::Unsupported,
};
let entry = McpAuthStatusEntry {
config,
auth_status,
None => McpAuthState::Unsupported,
};
let entry = McpAuthStatusEntry { config, auth_state };
(name, entry)
}
});
@@ -249,17 +246,17 @@ async fn compute_auth_status(
keyring_backend_kind: AuthKeyringBackendKind,
has_runtime_auth: bool,
runtime_context: &McpRuntimeContext,
) -> Result<McpAuthStatus> {
) -> Result<McpAuthState> {
if !config.enabled {
return Ok(McpAuthStatus::Unsupported);
return Ok(McpAuthState::Unsupported);
}
if has_runtime_auth {
return Ok(McpAuthStatus::BearerToken);
return Ok(McpAuthState::BearerToken);
}
match &config.transport {
McpServerTransportConfig::Stdio { .. } => Ok(McpAuthStatus::Unsupported),
McpServerTransportConfig::Stdio { .. } => Ok(McpAuthState::Unsupported),
McpServerTransportConfig::StreamableHttp {
url,
bearer_token_env_var,
+1 -1
View File
@@ -567,7 +567,7 @@ fn auth_statuses_from_entries(
) -> HashMap<String, McpAuthStatus> {
auth_status_entries
.iter()
.map(|(name, entry)| (name.clone(), entry.auth_status))
.map(|(name, entry)| (name.clone(), McpAuthStatus::from(entry.auth_state)))
.collect::<HashMap<_, _>>()
}