Skip to content

fix(mcp): survive a cross-process OAuth refresh race on connect - #40768

Open
justprosh wants to merge 3 commits into
anomalyco:v2from
justprosh:fix/v2-mcp-oauth-refresh-race
Open

fix(mcp): survive a cross-process OAuth refresh race on connect#40768
justprosh wants to merge 3 commits into
anomalyco:v2from
justprosh:fix/v2-mcp-oauth-refresh-race

Conversation

@justprosh

Copy link
Copy Markdown

Issue for this PR

Closes #34520

Type of change

  • Bug fix

What does this PR do?

Two opencode processes share one credential row per MCP server, so both can hold the same refresh
token. The first refresh rotates it and saves the new tokens; the second one fails with the token it
still had, and its invalidate hook deletes the whole row — throwing away the tokens the first one
just wrote, so the server drops to needs_auth right after a successful refresh.

The connect provider trusted the snapshot it took when it was built. Now tokens() re-reads the row,
so a rotation done elsewhere is picked up, and invalidate re-reads before deleting: a row that
changed since we last presented tokens is kept, a row that really is the rejected one is still gone.

No locking is added — that is the wider single-flight work in #34520.

How did you verify your code works?

  • New test in packages/core/test/mcp-oauth.test.ts: one connect against an auth-gated server whose
    token endpoint rejects, with the credential rotated by another process mid-refresh. Fails on v2,
    passes with the fix.
  • bun test test/mcp.test.ts test/mcp-oauth.test.ts in packages/core — 23 pass, tsgo --noEmit clean.
  • Against a live MCP server: connect refreshed the expired token and wrote it back to the same
    credential row instead of deleting it.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

The connect-time provider snapshots the stored credential when it is
built and serves tokens from that snapshot. With several processes
sharing the credential store, two of them can hold the same refresh
token; the first refresh rotates it and updates the row, and the
loser's refresh then fails with the stale token. Its invalidate hook
removes the whole credential row, destroying the fresh tokens the
winner just saved, and the server drops to needs_auth even though
working credentials existed a moment earlier.

Re-read the row on every tokens() call so a rotation done elsewhere is
picked up, and make invalidate keep a row that has rotated since this
provider last presented it.

Ref anomalyco#34520 anomalyco#34074
Drives one connect against an auth-gated server whose token endpoint always
rejects. Another process rotates the shared credential while the first refresh
is in flight; the retry must present the rotated token, which only happens if
the rejected refresh left the row alone.
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

I found one potentially related PR:

#37058 - fix(xai): cross-process single-flight for OAuth refresh

This PR addresses a similar cross-process OAuth refresh race condition for the xAI provider. The current PR (40768) is fixing the same type of issue for MCP servers. Both deal with credential rotation happening across multiple processes, though they address different providers/contexts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant