MCP OAuth callback URL override is not used as an exact redirect URI

Open 💬 4 comments Opened Jun 28, 2026 by sshishigin

What issue are you seeing?

mcp_oauth_callback_url is documented and configurable as an optional redirect URI override for MCP OAuth login, but Codex still appends a server-derived callback id to the configured URL before sending it as redirect_uri.

For example, with config overrides equivalent to:

mcp_oauth_callback_port = 10230
mcp_oauth_callback_url = "http://localhost:10230/callback"

Codex builds an authorization URL whose redirect_uri is effectively:

http://localhost:10230/callback/<callback_id>

This breaks OAuth providers that require exact redirect URI matching, such as enterprise OIDC/Keycloak clients registered with a fixed localhost callback URL. The provider rejects the request before the authorization-code exchange with an error like:

invalid_request: The requested redirect_uri is missing in the client configuration.

The configured callback URL is valid for the provider, but the mutated URL with the extra path segment is not.

What steps can reproduce the bug?

  1. Configure a streamable HTTP MCP server that uses OAuth with an OIDC provider whose client has only this redirect URI registered:
http://localhost:10230/callback
  1. Run MCP login with explicit callback settings:
codex mcp login <server> \
  -c mcp_oauth_callback_port=10230 \
  -c 'mcp_oauth_callback_url="http://localhost:10230/callback"'
  1. Inspect or open the authorization URL emitted by Codex.

Actual result: the redirect_uri includes an additional server-derived path segment after /callback, so strict OIDC clients reject it.

A small branch that demonstrates the proposed behavior is available here:

https://github.com/sshishigin/codex/tree/codex-respect-mcp-oauth-callback-url

Commit:

3560426d05 Respect configured MCP OAuth callback URL

What is the expected behavior?

When mcp_oauth_callback_url is explicitly configured, Codex should use it exactly as the OAuth redirect_uri.

The existing callback-id behavior can remain for generated local redirect URIs, where Codex chooses the listener address and callback path itself. The surprising part is mutating an explicit redirect URI override.

Additional information

This appears to be in codex-rs/rmcp-client/src/perform_oauth_login.rs: resolve_redirect_uri(...) returns the configured callback URL, and then append_callback_id_to_redirect_uri(...) is applied unconditionally.

Suggested high-level fix:

  • if callback_url / mcp_oauth_callback_url is set, use it as-is;
  • otherwise keep appending the server-derived callback id to generated local callback URLs.

Local validation of the proposed patch:

cargo fmt -p codex-rmcp-client
cargo test -p codex-rmcp-client callback

Environment used while diagnosing:

Codex CLI: codex-cli 0.0.0 local build
Platform: Darwin 25.5.0 arm64 arm
Terminal: Apple Terminal

I read docs/contributing.md and understand unsolicited PRs are not accepted. I am opening this issue first with root-cause analysis and a high-level fix outline; the branch is provided only as a concrete reference if maintainers want it.

View original on GitHub ↗

4 Comments

sshishigin · 2 months ago

Related public MCP OAuth reports I found while debugging this:

  • #13200 tracks Slack official MCP / pre-registered OAuth client support. A recent comment there also notes that Codex appends a callback id to configured callback URLs, requiring wildcard redirects.
  • #19154 tracks private/enterprise MCP servers that require pre-registered OAuth clients instead of Dynamic Client Registration.
  • #24103 tracks Meta Ads official MCP failing before browser auth because dynamic registration is unavailable/rejected.
  • #24058 covers WebIDE/code-server MCP OAuth callback constraints.

This issue is narrower: even when the OAuth client id and callback port/url are already configurable, is not treated as an exact redirect URI. Fixing that should help the static/pre-registered-client cases where providers require exact redirect URI matching.

sshishigin · 2 months ago

Small correction to my previous comment: the omitted config key in the last paragraph is mcp_oauth_callback_url.

wgu9 · 1 month ago

I checked current main (80f54d1) and this looks like a precise path-construction mismatch in the MCP OAuth helper.

Relevant owner path: codex-rs/rmcp-client/src/perform_oauth_login.rs.

The code currently does both:

  • resolve_redirect_uri(server, callback_url) returns the configured callback URL as the redirect URI
  • append_callback_id_to_redirect_uri(redirect_uri, callback_id) appends a server-derived path segment before query params

There are tests for appending the callback id and for preserving query placement, but the reported issue is about wanting the configured callback URL to be used as an exact registered redirect URI. If an OAuth provider has strict redirect URI matching, appending /{callback_id} turns a valid configured URL into a different URI.

A conservative fix shape would be to split the concepts:

  • the registered redirect URI sent to the OAuth provider
  • the local/host callback route Codex listens for or matches
  • the anti-confusion server binding token currently encoded as callback_id

Regression test: configure callback_url = "https://callbacks.example.com/oauth/callback", assert the authorize request uses exactly that URI, and separately assert callback validation still binds the response to the intended MCP server.

Poent · 14 days ago

Adding a concrete reproduction against a public hosted MCP server: ElevenLabs Hosted MCP.

Environment

  • Windows 11
  • Codex Desktop / CLI 0.147.0-alpha.6.6
  • MCP endpoint: https://api.us.elevenlabs.io/v1/mcp
  • Transport: Streamable HTTP
  • ElevenLabs' published OAuth client ID: dee9ba45-a592-4730-a321-a7b90a00c436

Configuration:

[mcp_servers.elevenlabs]
url = "https://api.us.elevenlabs.io/v1/mcp"
scopes = ["convai_read", "convai_write", "text_to_speech", "speech_history_read"]

[mcp_servers.elevenlabs.oauth]
client_id = "dee9ba45-a592-4730-a321-a7b90a00c436"

Login command:

codex mcp login elevenlabs --scopes convai_read,convai_write,text_to_speech,speech_history_read

Observed progression

  1. Without the explicit client ID, Codex attempts dynamic client registration and fails because ElevenLabs does not support DCR:

``text
Registration failed: Dynamic client registration not supported
``

  1. Adding the published client ID correctly bypasses DCR and opens the authorization URL.
  1. Codex sends a loopback redirect shaped like:

``text
http://127.0.0.1:63546/callback/6MFBM1In3Ey-
``

The 6MFBM1In3Ey- suffix is exactly the deterministic callback ID Codex derives from https://api.us.elevenlabs.io/v1/mcp.

  1. ElevenLabs rejects the request before consent with:

``text
Authorization Error
Invalid redirect_uri
``

Codex subsequently times out waiting for a callback that the provider never sends.

Working control case

The same ElevenLabs account, endpoint, published client ID, and scopes authenticate successfully through Claude. Claude sends its registered hosted callback:

https://claude.ai/api/mcp/auth_callback

The ElevenLabs consent screen then opens normally and identifies the OAuth application as “Claude — Official ElevenLabs connector for Claude.”

This confirms the ElevenLabs OAuth service and account are working; the interoperability failure is specific to the redirect URI produced by Codex.

Relevant provider documentation:

Why this issue blocks the integration

ElevenLabs provides a fixed/pre-registered client and no DCR endpoint. Codex can now accept that static client ID, but it unconditionally appends the server-specific callback ID to both generated and explicitly configured callback URLs. Therefore, there is no native Codex configuration that can send an exact pre-registered redirect URI for this integration.

A compatible fix could preserve the callback-ID behavior by default while providing an explicit exact-redirect mode for static/pre-registered OAuth clients, or provide a Codex-hosted callback flow.

This reproduces on a current Codex build, and current main still appears to append the callback ID unconditionally.