mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
[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:
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();
|
||||
|
||||
|
||||
Reference in new issue
Block a user