🤖 fix: preserve MCP OAuth authorization-server binding across restarts#3710
Conversation
@ai-sdk/mcp auth() saves tokens/clientInformation with authorization_server + token_endpoint attached and requires them back before using a stored refresh_token; when missing it invalidates the tokens (deleting the refresh token) and demands interactive re-login. parseStoredCredentials rebuilt credentials with an allow-list that dropped both fields, so the first 401 after an app restart wiped the refresh token and forced users to re-login to remote MCP servers. Persist both fields (validated as a URL pair) through the store round-trip and add regression tests.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fec62e5ac3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Mirror (and tighten) @ai-sdk/mcp SafeUrlSchema: drop javascript:/data:/ vbscript: and other non-http(s) schemes so corrupted store values self-heal instead of surfacing as an auth() metadata mismatch.
|
@codex review |
|
Codex Review: Didn't find any major issues. You're on a roll. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Summary
Fixes MCP OAuth token refresh breaking after every app restart: the persisted credential store stripped the
authorization_server/token_endpointbinding fields that@ai-sdk/mcprequires before it will use a storedrefresh_token, causing the SDK to delete the refresh token and demand interactive re-login on the first 401.Background
@ai-sdk/mcpv2 binds saved tokens/client information to the authorization server:auth()appendsauthorization_server+token_endpointbefore callingsaveTokens()/saveClientInformation(), and requires them back before running therefresh_tokengrant. When the binding is missing,auth()callsinvalidateCredentials("tokens")(deleting the still-valid refresh token), skips refresh entirely, and falls through to interactive authorization — which Mux's background provider intentionally rejects with "MCP OAuth login required".Mux's
parseStoredCredentialsrebuilt tokens/client info from disk with a strict allow-list that predates these SDK fields, so they were dropped on every store load. Within one app session refresh worked (the in-memory object kept the fields by reference), but after a restart the first expired access token permanently wiped the refresh token. User-visible symptom: remote MCP servers keep asking for re-login after restarting Mux.Implementation
MCPOAuthTokens/MCPOAuthClientInformationnow include the SDK's optionalauthorization_server/token_endpointfields, with comments flagging the allow-list coupling for future SDK upgrades.parseAuthorizationServerBinding()helper inmcpOauthService.tspreserves the pair through the store round-trip. Defensive by design: values must passURL.canParse(a corrupted store entry can't make the SDK'snew URL(...)throw mid-auth), and the pair is atomic — a partial/invalid binding is dropped entirely since the SDK requires both fields.saveTokenspersists the SDK's full object), so this fix self-heals existing installs whose on-disk store still contains the fields. Users whose refresh tokens were already invalidated by the bug need one final interactive login.Validation
tokens()/clientInformation()— exactly whatauth()consults before refreshing; (2) corrupt/partial bindings are dropped as a pair while the refresh token is kept.@ai-sdk/mcp2.0.10 source that the persisted shape (OAuthTokensSchema/OAuthClientInformationSchema) includes both fields and thatgetStoredAuthorizationServerInformation()consults tokens first, then client info.Risks
Low. The change only widens what the store parser preserves; all previously-accepted stores still parse identically. Worst case for a malformed binding is the pre-existing behavior (binding dropped → interactive re-login).
Generated with
mux• Model:anthropic:claude-fable-5• Thinking:xhigh• Cost:$5.66