Skip to content

feat: add OAuth 2.1 authorization for remote MCP servers#962

Open
ital0 wants to merge 42 commits into
mainfrom
italomenezes/thu-574-add-oauth-21-authorization-for-mcp-servers
Open

feat: add OAuth 2.1 authorization for remote MCP servers#962
ital0 wants to merge 42 commits into
mainfrom
italomenezes/thu-574-add-oauth-21-authorization-for-mcp-servers

Conversation

@ital0

@ital0 ital0 commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Note

High Risk
Introduces a full OAuth client (PKCE, DCR, callback validation, token refresh) and persists tokens on-device; mistakes in issuer pinning or state handling could weaken authorization, though the change is scoped to MCP server credentials rather than app login.

Overview
Adds OAuth 2.1 authorization for remote MCP servers alongside existing bearer tokens: on-device mcp_secrets now stores full OAuth token sets (access/refresh, issuer, DCR client_id), and connections refresh tokens through the proxy before building MCP headers.

A new mcp-auth stack drives discovery (RFC 9728/8414), PKCE + DCR via the MCP SDK, callback checks (CSRF state + RFC 9207 iss), and platform-specific redirects—web full-page callback, mobile system browser + App Link, desktop Tauri loopback with inline token exchange. OAuth callbacks and deep links now forward iss for MCP validation; backend CORS expose headers add WWW-Authenticate for cross-origin auth challenges.

The MCP Servers settings UI probes URLs (debounced), classifies 401s (rejected PAT vs “Add & Authorize” vs static token only), runs Add & Authorize with rollback on failure, and shows per-server Authorize / Re-authorize states. MCPProvider treats 401s as “needs auth” rather than hard failures when appropriate.

Reviewed by Cursor Bugbot for commit 3655cc5. Bugbot is set up for automated code reviews on this repo. Configure here.

ital0 added 30 commits June 2, 2026 17:56
- A remote MCP server can drop its transport between sends; the next
  tools() call then rejects and silently kills tool discovery for that
  server. Detect that close signal and reconnect once before retrying,
  so a transient drop no longer disables the server for the session.
- Add isClosedConnectionError to match the @ai-sdk/mcp close signal by
  name/message, since MCPClientError isn't exported for instanceof.
- mergeMcpTools reconnects once and retries; a failed reconnect or a
  second failure skips only that server so discovery never blocks send.
- Provider tracks a client to serverId reverse map and exposes
  reconnectClient; reconnectServer is now coalesced and idempotent and
  re-checks serversRef after the await to drop orphaned connections.
- Thread reconnectClient through the chat store and ACP adapter context.
…support-authenticated-remote-mcp-servers-bearer-api-key

# Conflicts:
#	src/types/acp.ts
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown

Semgrep Security Scan

No security issues found.

Comment thread src/settings/mcp-servers.tsx Outdated
Comment thread src/settings/mcp-servers.tsx Outdated
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown

PR Metrics

Metric Value
Lines changed (prod code) +1982 / -51
JS bundle size (gzipped) 🟢 732.1 KB → 732.7 KB (+625 B, +0.1%)
Test coverage 🟢 77.17% → 77.62% (+0.5%)
Performance (preview) Preview not ready — Render deploy may have timed out
Accessibility
Best Practices
SEO

Updated Thu, 11 Jun 2026 19:24:18 GMT · run #1856

@ital0 ital0 self-assigned this Jun 9, 2026
Base automatically changed from italomenezes/thu-573-support-authenticated-remote-mcp-servers-bearer-api-key to main June 10, 2026 12:23
…add-oauth-21-authorization-for-mcp-servers

# Conflicts:
#	backend/src/db/powersync-schema.ts
#	src/dal/index.ts
#	src/dal/mcp-secrets.test.ts
#	src/dal/mcp-secrets.ts
#	src/dal/mcp-servers.test.ts
#	src/db/powersync/schema.ts
#	src/lib/mcp-errors.test.ts
#	src/lib/mcp-errors.ts
#	src/lib/mcp-provider.test.tsx
#	src/lib/mcp-provider.tsx
#	src/settings/mcp-servers.test.tsx
#	src/settings/mcp-servers.tsx
Comment thread src/lib/mcp-auth/mcp-oauth-state.ts Dismissed
@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown

Preview environment deployed 🚀

Service URL
Marketing / blog / docs https://thunderbolt-pr-962.preview.thunderbolt.io
App https://app-pr-962.preview.thunderbolt.io
API https://api-pr-962.preview.thunderbolt.io
Keycloak https://auth-pr-962.preview.thunderbolt.io
PowerSync https://powersync-pr-962.preview.thunderbolt.io

Stack: preview-pr-962 · Commit: 3655cc56db80ce5d6b93448d7789a4dcaf2b9f4e

Auto-destroys on PR close/merge. Login via the bundled Keycloak realm — demo@thunderbolt.io / demo by default.


// OAuth creds (token expired/revoked) or no creds (OAuth-eligible by
// precedence) → re-authorize.
return { phase: 'needs-auth' }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Authorize shown for token-only servers

Medium Severity

The add-dialog probe uses classifyMcpServerAuth to label servers that advertise OAuth but lack DCR/CIMD as token-only, yet deriveOAuthCardDecision still maps a stored no-credential 401 to needs-auth. The MCP server card then shows Authorize even when the web OAuth flow cannot register a client and will fail.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 632b429. Configure here.

Comment thread src/settings/mcp-servers.tsx Outdated
}, 700)
return () => clearTimeout(timer)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [newServerUrl])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Auto-probe runs dialog closed

Medium Severity

The debounced URL auto-detect useEffect keys only on newServerUrl, not whether the Add dialog is open. A pending timer still calls testConnection after the dialog closes, triggering proxy MCP probes and updating add-form state in the background.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 78e4ee1. Configure here.

setIsTestingConnection(false)
if (probeIdRef.current === probeId) {
setIsTestingConnection(false)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale probe wrong URL

High Severity

In-flight connection probes are only invalidated when a newer probe bumps probeId, not when the URL field changes. Each probe also resets testResult to idle, so resetConnectionTest skips on edit while a test runs. A slow probe can still commit success or OAuth hints for the old URL while the dialog shows a different URL, enabling Add or Add & Authorize for an untested endpoint.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit effeb3f. Configure here.

Comment thread src/settings/mcp-servers.tsx
@darkbanjo darkbanjo self-requested a review June 11, 2026 18:56

async saveTokens(tokens: OAuthTokens): Promise<void> {
const existing = await getMcpServerCredentials(this.db, this.serverId)
const base = existing?.type === 'oauth' ? existing : { type: 'oauth' as const, access_token: '' }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

if there's no existing oauth cred here, base is just { type: 'oauth', access_token: '' }, so we save tokens without the issuer/tokenEndpoint/clientId binding. fine today since completeMcpOAuthFlow writes the full blob directly — but if completion ever routes through the SDK's auth() path, refresh would silently break into a re-auth loop. mind carrying the binding fields here, or dropping a comment that this is a deliberate placeholder for now?

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

There are 6 total unresolved issues (including 3 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3655cc5. Configure here.

dispatch({ type: 'set-add-authorize-pending', pending: true })
try {
await createRow()
setOAuthState({ returnContext: '/settings/mcp-servers' })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Shared OAuth return context collision

Medium Severity

MCP authorization only merges returnContext into the same oauth_flow_state store integrations use. Starting a second OAuth flow overwrites where the callback lands, so an MCP code can open Integrations (and vice versa) and neither handler completes the exchange.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3655cc5. Configure here.

// Always clear the navigation state so a refresh can't reprocess the callback.
deps.clearNavState()
if (!serverId) {
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Clears OAuth nav without MCP flow

Medium Severity

On the MCP Servers page, any location.state.oauth payload triggers clearNavState() before checking for a pending MCP handshake. If the callback was meant for Integrations (or the handshake is missing), the code is removed from history without being exchanged.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3655cc5. Configure here.

issuer: metadata.issuer,
tokenEndpoint: metadata.token_endpoint,
scope: tokens.scope,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Token exchange drops refresh token

Low Severity

completeMcpOAuthFlow writes refresh_token: tokens.refresh_token with no fallback when the authorization server omits a new refresh token on exchange, unlike saveTokens and ensureValidMcpOAuthToken, which keep the previous refresh token.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3655cc5. Configure here.

@ital0 ital0 deployed to preview June 11, 2026 19:22 — with GitHub Actions Active
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.

3 participants