Skip to content

MCP server OAuth

When an MCP server needs OAuth, Zimmer runs the whole flow itself (discovery, registration, PKCE, token exchange, refresh) and then writes the tokens into the agent CLI’s own credential file so the agent’s MCP client finds them.

That last step is why this is harder than it sounds: Zimmer has to produce a file in a format that another vendor’s private code will read.

Before spawning, McpOauthCredentialInjector#check_credentials_status looks at every remote MCP server on the session (http / streamable-http / sse):

A session that needs OAuth fails fast: it goes to failed with failure_reason: oauth_required instead of hanging or prompting, and the UI turns that into Authorize buttons. Completing the flow resumes it.

The credential key is a copy of Claude Code’s private algorithm

Section titled “The credential key is a copy of Claude Code’s private algorithm”

To make the agent’s MCP client find the token, Zimmer must key it exactly the way Claude Code keys it. McpOauthCredential.compute_credential_key:

"#{server_name}|#{SHA256(compact_json({type, url, headers}))[0,16]}"

…where “compact JSON” is faked by string-munging ": "":" and ", "",", and streamable-http is normalized to http.

And it only exists because of two open Codex bugs

Section titled “And it only exists because of two open Codex bugs”

CodexMcpCredentialWriter’s header explains why Zimmer rewrites Codex’s entire MCP credential store on every session spawn:

  • openai/codex#15122 — credentials from codex mcp login don’t persist across restarts.
  • openai/codex#17265 — Codex won’t use the stored refresh token, so MCP calls fail with “Authorization required.”

So Zimmer refreshes the tokens itself every 30 minutes and re-writes them at spawn, so Codex never has to. It’s a workaround for someone else’s bugs, and it will need to be removed when they’re fixed.

RefreshMcpOauthTokensJob, every 30 minutes. It refreshes credentials expiring within an hour — but throttled by PROACTIVE_REFRESH_MIN_INTERVAL (won’t touch anything updated in the last 4 hours), deliberately, to reduce exposure to rotating-refresh-token reuse detection.

It splits network errors carefully:

  • Retryable — the connection was never established, so the server never saw the request. Safe to retry in-band.
  • Ambiguous — the request went out and the response was lost. Never retried in-band; deferred to the next cron run. Retrying could burn a single-use refresh token.

That distinction is the kind of care that’s easy to skip and expensive to skip.

On a permanent failure (invalid_grant / invalid_client / unauthorized_client) it nulls the refresh token but keeps a still-valid access token instead of force-expiring it.