Codex Desktop does not refresh OAuth-backed MCP access token before startup; keeps sending expired bearer from Keychain

Open 💬 5 comments Opened Jun 9, 2026 by tonyloks

Summary

Codex Desktop / bundled Codex CLI fails to refresh an OAuth-backed Streamable HTTP MCP server before startup/tool discovery, even though the stored refresh token is present and valid.

Instead, Codex reads the expired access token from macOS Keychain and sends it to /mcp as Authorization: Bearer .... The MCP server returns 401 Invalid or expired access token. Codex does not call the advertised /oauth/token endpoint, so the MCP tools are not exposed.

This looks closely related to #17265, #26237, #26522, and #26523, but I am adding a concrete end-to-end trace with both Codex logs and server-side logs.

Environment

  • Product: Codex Desktop App on macOS
  • Bundled Codex CLI: codex-cli 0.137.0-alpha.4
  • PATH Codex CLI: codex-cli 0.135.0 (not the failing runtime)
  • Credential store: macOS Keychain, service Codex MCP Credentials
  • MCP server transport: streamable_http
  • MCP server auth: OAuth, no static bearer_token_env_var
  • Access token TTL: 3600 seconds
  • Refresh token TTL: 30 days
  • MCP config shape:
[mcp_servers.dam]
url = "https://direct-alert.ru/mcp"

codex mcp list --json reports this server as OAuth-backed:

{
  "name": "dam",
  "transport": {
    "type": "streamable_http",
    "url": "https://direct-alert.ru/mcp",
    "bearer_token_env_var": null,
    "http_headers": null,
    "env_http_headers": null
  },
  "auth_status": "o_auth"
}

Stored credential state

The local Keychain item exists under service Codex MCP Credentials for this MCP server. It contains both an access token and a refresh token.

Sanitized details:

server_name: dam
url: https://direct-alert.ru/mcp
client_id: mcp_SfV2_SWHRrmg8skr6xFJ51BuYbwdloxe
access_token_sha256_prefix16: 666fece093144e0c
refresh_token_sha256_prefix16: b5a3ba9ca5cfeeb5
expires_at: 2026-06-08T14:38:29.780Z
scope: mcp:tools
token_type: bearer

Server-side DB confirms the same token state:

access  fp=666fece093144e0c user_id=1 expires_at=2026-06-08 14:38:29.777816 revoked_at=null
refresh fp=b5a3ba9ca5cfeeb5 user_id=1 expires_at=2026-07-08 13:38:29.777680 revoked_at=null

There are no newer access tokens for this user/client after 2026-06-08 13:38:29Z.

OAuth metadata is valid

The protected resource metadata advertises the authorization server:

{
  "resource": "https://direct-alert.ru/mcp",
  "authorization_servers": ["https://direct-alert.ru"],
  "bearer_methods_supported": ["header"],
  "resource_name": "direct-alert-mcp",
  "scopes_supported": ["mcp:tools"]
}

The authorization server metadata advertises the token endpoint:

{
  "issuer": "https://direct-alert.ru",
  "authorization_endpoint": "https://direct-alert.ru/oauth/authorize",
  "token_endpoint": "https://direct-alert.ru/oauth/token",
  "registration_endpoint": "https://direct-alert.ru/oauth/register",
  "revocation_endpoint": "https://direct-alert.ru/oauth/revoke"
}

Reproduction

  1. Configure an OAuth-backed Streamable HTTP MCP server.
  2. Run codex mcp login <server> and complete OAuth.
  3. Confirm the Keychain credential contains both access_token and refresh_token.
  4. Wait until the stored access token expires while the refresh token remains valid.
  5. Start a fresh Codex Desktop / bundled CLI run that needs MCP tool discovery.

I reproduced the fresh startup path with:

/Applications/Codex.app/Contents/Resources/codex exec \
  --cd /Users/antonperepecaev/Desktop/projects/direct-alert \
  --sandbox read-only \
  --json \
  'Check whether the DAM MCP is available; do not create or modify files.'

Observed Codex behavior

Bundled Codex stderr shows an auth failure during MCP startup:

ERROR rmcp::transport::worker: worker quit with fatal: Transport channel closed, when AuthRequired(AuthRequiredError { www_authenticate_header: "Bearer resource_metadata=\"https://direct-alert.ru/.well-known/oauth-protected-resource/mcp\"" })

WARN codex_mcp::rmcp_client: failed to initialize MCP client during shutdown: MCP startup failed: handshaking with MCP server failed: Send message error Transport [...] error: Auth required, when send initialize request

The MCP tools are not exposed to the session.

Server-side trace for the same startup window

Fresh startup window: 2026-06-09T10:19:41Z..10:21:00Z.

Server logs:

10:19:50.243 GET  /mcp                                      -> 401, no bearer token
10:19:50.277 GET  /.well-known/oauth-protected-resource/mcp -> 200
10:19:50.310 GET  /.well-known/oauth-authorization-server   -> 200
10:19:50.464 POST /mcp                                      -> 401 Invalid or expired access token, access_token_fingerprint=666fece093144e0c

Critically, there is no /oauth/token request in that window.

So Codex successfully discovers the OAuth metadata, learns the correct token endpoint, but then sends the expired stored access token to /mcp instead of refreshing via /oauth/token.

Why this appears Codex-side

  • The server exposes a valid token_endpoint in OAuth metadata.
  • The refresh token is present, not revoked, and not expired.
  • The access token fingerprint sent by Codex exactly matches the expired access token stored in Keychain.
  • No /oauth/token request reaches the server during startup/tool discovery.
  • Other MCP transport types work because they do not use this path:
  • stdio MCPs do not use OAuth refresh;
  • local Streamable HTTP with bearer_token_env_var uses a static bearer;
  • only OAuth-backed Streamable HTTP MCP fails after access token expiry.

Expected behavior

Before MCP initialize/tool discovery, Codex should detect that the persisted OAuth access token is expired or near expiry, call the advertised token endpoint with grant_type=refresh_token, persist the new token response, and then initialize the MCP server with the fresh access token.

If the server returns 401 invalid_token during initialize, Codex should refresh once using the stored refresh token and retry initialize, or surface a clear reauth-required error if refresh fails.

Actual behavior

Codex does not call /oauth/token; it sends the expired Keychain access token to /mcp, receives 401, and the MCP tools are unavailable.

Manual re-login fixes the problem temporarily until the next access-token expiry.

View original on GitHub ↗

This issue has 5 comments on GitHub. Read the full discussion on GitHub ↗