## Summary
- classify authentication-required RMCP startup failures, including
errors nested inside `ClientInitializeError::TransportError`
- let `codex-mcp` consume that classification so the existing
`reauthenticationRequired` startup failure reason is emitted
- add a regression test that performs real startup with an expired
persisted OAuth token and no refresh token
## Why
Follow-up to #29877.
RMCP stores streamable HTTP initialization failures inside a dynamic
transport error whose payload is not exposed through the standard Rust
error source chain. The original `anyhow::Error::chain()` check
therefore missed the nested `AuthError::AuthorizationRequired` seen
during real MCP startup and emitted `failureReason: null`.
The transport-specific inspection now lives in `codex-rmcp-client`,
while `codex-mcp` consumes only the domain-level authentication-required
result. This classifier does not distinguish first-time login from
reauthentication; the existing auth-state logic remains responsible for
that distinction.
## User impact
When stored MCP OAuth credentials are expired and cannot be refreshed,
app clients now receive `failureReason: "reauthenticationRequired"` on
the failed startup update and can show the reconnect action. First-time
login and unrelated startup failures remain unchanged.
## Validation
- `just test -p codex-rmcp-client --test streamable_http_oauth_startup
identifies_expired_unrefreshable_token_startup_error`
- `just test -p codex-mcp
startup_outcome_error_identifies_authentication_required`
- `just test -p codex-mcp
mcp_startup_failure_reason_requires_existing_oauth_and_auth_failure`
- `cargo build -p codex-cli --bin codex`
- local app-server probe emitted `failureReason:
"reauthenticationRequired"`
- manual end-to-end reconnect flow confirmed
- `just fmt`
## 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`
## Summary
- store MCP OAuth credentials in the configured auth credential backend
- support encrypted-local OAuth storage, including legacy keyring
migration
- propagate the credential backend through MCP refresh, session, CLI,
and app-server paths
## Stack
1. #27504 — config and feature flag
2. #27535 — auth-specific secret namespaces
3. #27539 — encrypted CLI auth storage
4. this PR — encrypted MCP OAuth storage
This is a parallel review stack; the original #17931 remains unchanged.
## Tests
- `just test -p codex-rmcp-client` (the transport round-trip test passed
after building the required `codex` binary and retrying)
- `just test -p codex-mcp`
- `just test -p codex-app-server
refresh_config_uses_latest_auth_keyring_backend`
- `just test -p codex-core
refresh_mcp_servers_is_deferred_until_next_turn`
- `just test -p codex-cli mcp`
- `just fix -p codex-rmcp-client -p codex-mcp -p codex-core -p codex-cli
-p codex-app-server -p codex-protocol`
- `just bazel-lock-check`
## Why
Persisted MCP OAuth credentials were reported as authenticated whenever
a credential record existed. An expired token without a usable refresh
token could therefore appear as `OAuth` even though startup could not
authenticate with it, leaving users with a misleading status instead of
a login prompt.
## What changed
- Classify stored OAuth credentials as missing, usable, or requiring
authorization.
- Reuse the existing refresh window so near-expiry credentials without a
refresh path are also treated as logged out.
- Validate required credential fields before reporting OAuth
authentication.
- Add unit coverage for credential usability and integration coverage
for expired, unexpired, and refreshable persisted credentials.
## Validation
- `just test -p codex-rmcp-client`
## Why
Codex persists OAuth expiry as an absolute `expires_at`, then
reconstructs RMCP’s relative `expires_in` when credentials are loaded.
For an already-expired token, Codex reconstructed `expires_in` as
missing.
[RMCP 0.15 treated a missing `expires_in` as zero when a refresh token
was
present](https://github.com/modelcontextprotocol/rust-sdk/blob/9cfc905a9ef17c8bba6748dc0a9bdd2452681733/crates/rmcp/src/transport/auth.rs#L704-L723),
so this still triggered a refresh. [RMCP 1.7 treats missing expiry
information as unknown and uses the access token
as-is](https://github.com/modelcontextprotocol/rust-sdk/blob/3529c3675ff64db805bd947ca6ece6090809e43d/crates/rmcp/src/transport/auth.rs#L1233-L1265),
causing the stale token to be sent during `initialize`.
## What changed
- Represent a known-expired persisted token as `expires_in = 0`,
preserving `None` for genuinely unknown expiry.
- Add Streamable HTTP coverage requiring the token to refresh before the
startup handshake.
## Validation
- The new regression test fails on RMCP 1.7 before the fix and passes
afterward.
- The same scenario passes on the commit immediately before the RMCP 1.7
update, using RMCP 0.15.
- `just test -p codex-rmcp-client` (63 passed).