Meta MCP OAuth blocked by issuer mismatch: support explicit per-server trusted issuer override
What feature would you like to see?
Add an explicit, per-MCP trusted OAuth issuer override for remote MCP servers whose protected-resource metadata points to an authorization-server URL that publishes a different issuer.
Suggested configuration shape:
[mcp_servers.meta-ads]
url = "https://mcp.facebook.com/ads"
oauth_expected_issuer = "https://www.facebook.com"
The override should be opt-in, scoped to one exact MCP server URL, require an absolute HTTPS issuer URL, and preserve strict issuer validation for every server without the setting. Codex should still validate the authorization response and token exchange against the configured issuer. A warning during login would make the exception visible.
What problem would this solve?
Meta's official hosted MCP endpoints currently publish internally inconsistent OAuth discovery metadata. Codex CLI 0.147.0 rejects both before opening the browser.
For Meta Ads:
GET https://mcp.facebook.com/.well-known/oauth-protected-resource/ads
resource: https://mcp.facebook.com/ads
authorization_servers[0]: https://mcp.facebook.com/ads
GET https://mcp.facebook.com/.well-known/oauth-authorization-server/ads
issuer: https://www.facebook.com
authorization_endpoint: https://www.facebook.com/v26.0/dialog/oauth
token_endpoint: https://graph.facebook.com/v26.0/oauth/access_token
For Meta Developer Tools:
GET https://mcp.facebook.com/.well-known/oauth-protected-resource/devtools
resource: https://mcp.facebook.com/devtools
authorization_servers[0]: https://mcp.facebook.com/devtools
GET https://mcp.facebook.com/.well-known/oauth-authorization-server/devtools
issuer: https://www.facebook.com
authorization_endpoint: https://www.facebook.com/v26.0/dialog/oauth
token_endpoint: https://graph.facebook.com/v26.0/oauth/access_token
Reproduction:
codex mcp add meta-ads --url https://mcp.facebook.com/ads
codex mcp login meta-ads
Actual result:
Error: Authorization server issuer mismatch: expected https://mcp.facebook.com/ads, received https://www.facebook.com
The same failure occurs for https://mcp.facebook.com/devtools.
Meta should ideally correct its discovery metadata. Until then, a per-server explicit override would provide a bounded compatibility path without weakening issuer checks globally. A same-registrable-domain heuristic would be too broad; the requested setting should compare exact configured URLs only.
Environment:
Codex CLI: 0.147.0
macOS: Apple Silicon
Transport: streamable_http
Authentication: OAuth
This is distinct from #35459, which reports a Meta Ads streamable-HTTP handshake failure after OAuth has already completed. This request concerns the earlier discovery/issuer-validation stage.
6 Comments
Update from a working local patch: an exact per-server issuer override for
https://www.facebook.comsuccessfully moves both Meta endpoints past the reported issuer mismatch. Meta Ads then reaches a separate server-side limitation:invalid_client_metadatabecause dynamic registration is unavailable, matching #24103. Meta Developer Tools proceeds to the browser authorization flow. This confirms the issuer override is narrowly useful but does not bypass OAuth client registration policy.Adding the spec grounding and the exact code seam for anyone implementing this, since both support the request as filed.
Codex's rejection is spec-correct; Meta's metadata is non-conformant. RFC 9728 defines
authorization_serversas a list of authorization-server issuer identifiers. Meta lists the resource URL itself (https://mcp.facebook.com/ads) as the issuer identifier, and then the AS metadata fetched from that identifier declaresissuer: https://www.facebook.com. RFC 8414 §3.3 requires theissuervalue in the retrieved metadata to be identical to the issuer identifier used to construct the well-known URL — so any conforming client will reject this pair, not just Codex. The durable fix is on Meta's side: publishauthorization_servers: ["https://www.facebook.com"]in the protected-resource metadata (and serve the AS metadata under that issuer). Worth reporting to them in parallel; until then an explicit override is the right bounded escape hatch.Where the check actually lives. The discovery + issuer validation is not in this repo's code but in the pinned upstream
rmcpcrate (rmcp = "=3.0.0"incodex-rs/Cargo.toml), insidermcp::transport::AuthorizationManager, whichcodex mcp logindrives here:https://github.com/openai/codex/blob/1f41cc5d92722748e45cae9cecc6d883a4e7cbb1/codex-rs/rmcp-client/src/perform_oauth_login.rs#L699-L707
Notably, that call site already relaxes one discovery strictness knob per policy —
auth_manager.set_allow_missing_issuer(true)— so there's precedent for exactly this kind of scoped relaxation. Implementingoauth_expected_issuermeans threading the config value fromMcpServerConfigto this call site plus either (a) a newrmcpAPI (set_expected_issuer(...)alongsideset_allow_missing_issuer) or (b) codex-side discovery that resolves metadata itself and hands rmcp pre-validated endpoints. (a) is smaller and benefits other rmcp consumers hitting the same Meta endpoints.Design constraints worth keeping from the proposal, plus two additions:
issparameter is already plumbed throughhandle_callback_with_issuer(perform_oauth_login.rs#L632-L639), so the override should become the expectediss, not disable the check.This is also an issue with gitlab, the problem is not unique to Meta
Error: Authorization server issuer mismatch: expected https://gitlab.com/api/v4/mcp, received https://gitlab.com
Another affected vendor: Namecheap's hosted MCP (
https://mcp.namecheap.com/mcp). It fails the same way, but it is a distinct sub-case from Meta and GitLab, and I think it matters for how the override is scoped.Namecheap's discovery metadata is internally consistent. Unlike Meta, it satisfies RFC 9728 and RFC 8414 §3.3:
Discovery validates cleanly. The violation is purely in the authorization response: the callback returns the host of the authorization endpoint rather than the declared issuer identifier, contrary to RFC 9207 §2.
Reproduction (codex-cli 0.147.0, macOS Apple Silicon, streamable_http):
Claude Code rejects it identically, so this is not client-specific:
Why the sub-case matters. For Meta the mismatch surfaces at discovery, so an override applied there is sufficient. For Namecheap discovery is fine and the mismatch only appears at the RFC 9207 callback check — so an implementation that only overrides the expected discovery issuer would not fix this endpoint. @jdcodes1's third constraint already covers it ("the override should become the expected
iss, not disable the check"), and I would just make that explicit as a requirement rather than a nicety: the setting has to reachhandle_callback_with_issuer, not only the discovery comparison.Also worth noting: Namecheap omits
authorization_response_iss_parameter_supportedfrom its AS metadata while sendingissanyway. RFC 9207 §2.4 requires clients to validateisswhenever it is present, so silently ignoring an unadvertisedisswould not be a safe general workaround either.Namecheap has no public repository for the hosted MCP (their GitHub org carries ILC,
terraform-provider-namecheap, andgo-namecheap-sdk, none of it MCP), so there is no upstream tracker to file against — which makes the bounded client-side override the only available path for this one until support routes it internally.+1 As it also blocks Claude Design MCP, which we be a nice addition as there is no equivalent with OpenAI so far.
For the GitLab report above: that one is a different root cause and is now filed separately as #40885.
GitLab's metadata agrees with itself —
authorization_servers[0]and the authorization server'sissuerare bothhttps://gitlab.com— so unlike Meta's endpoints there is nothing non-conformant to override. Codex appears to take the expected issuer from the resource URL instead ofauthorization_servers, which rejects a conformant server. Worth keeping distinct, since a trusted-issuer override would mask that rather than fix it.