Skip to content

🤖 fix: preserve MCP OAuth authorization-server binding across restarts#3710

Merged
ThomasK33 merged 2 commits into
mainfrom
oauth-mux-qwvw
Jul 10, 2026
Merged

🤖 fix: preserve MCP OAuth authorization-server binding across restarts#3710
ThomasK33 merged 2 commits into
mainfrom
oauth-mux-qwvw

Conversation

@ThomasK33

Copy link
Copy Markdown
Member

Summary

Fixes MCP OAuth token refresh breaking after every app restart: the persisted credential store stripped the authorization_server / token_endpoint binding fields that @ai-sdk/mcp requires before it will use a stored refresh_token, causing the SDK to delete the refresh token and demand interactive re-login on the first 401.

Background

@ai-sdk/mcp v2 binds saved tokens/client information to the authorization server: auth() appends authorization_server + token_endpoint before calling saveTokens() / saveClientInformation(), and requires them back before running the refresh_token grant. When the binding is missing, auth() calls invalidateCredentials("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 parseStoredCredentials rebuilt 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 / MCPOAuthClientInformation now include the SDK's optional authorization_server / token_endpoint fields, with comments flagging the allow-list coupling for future SDK upgrades.
  • New parseAuthorizationServerBinding() helper in mcpOauthService.ts preserves the pair through the store round-trip. Defensive by design: values must pass URL.canParse (a corrupted store entry can't make the SDK's new URL(...) throw mid-auth), and the pair is atomic — a partial/invalid binding is dropped entirely since the SDK requires both fields.
  • The write path was never broken (saveTokens persists 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

  • New regression tests: (1) binding fields survive a disk round-trip and are visible through the background provider's tokens() / clientInformation() — exactly what auth() consults before refreshing; (2) corrupt/partial bindings are dropped as a pair while the refresh token is kept.
  • Verified against the bundled @ai-sdk/mcp 2.0.10 source that the persisted shape (OAuthTokensSchema / OAuthClientInformationSchema) includes both fields and that getStoredAuthorizationServerInformation() 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

@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.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/node/services/mcpOauthService.ts Outdated
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.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: 8e2ce1b495

ℹ️ 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".

@ThomasK33 ThomasK33 added this pull request to the merge queue Jul 10, 2026
Merged via the queue into main with commit e7ee255 Jul 10, 2026
24 checks passed
@ThomasK33 ThomasK33 deleted the oauth-mux-qwvw branch July 10, 2026 22:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant