Skip to content

feat(mcp): OAuth 2.1 + PKCE for outbound MCP servers#4441

Open
waleedlatif1 wants to merge 7 commits into
stagingfrom
waleedlatif1/mcp-oauth
Open

feat(mcp): OAuth 2.1 + PKCE for outbound MCP servers#4441
waleedlatif1 wants to merge 7 commits into
stagingfrom
waleedlatif1/mcp-oauth

Conversation

@waleedlatif1
Copy link
Copy Markdown
Collaborator

Summary

  • Adds spec-compliant OAuth 2.1 + PKCE support for outbound MCP servers via the SDK's OAuthClientProvider
  • Auto-detects OAuth requirement on server create via unauthenticated probe (WWW-Authenticate / oauth-protected-resource)
  • Persists per-user-per-server tokens (encrypted) in new mcp_server_oauth table; SDK refreshes automatically before expiry
  • Popup-based consent flow (/api/mcp/oauth/start/api/mcp/oauth/callback) with state CSRF protection
  • Pre-registered OAuth client support (Client ID + Secret in Advanced settings) for servers without RFC 7591 DCR
  • Surfaces reauth_required from tool execution when refresh token is invalid so the UI can prompt to reconnect

Type of Change

  • New feature

Testing

Tested manually against OAuth-protected MCP servers (Linear). Existing header-auth servers regression-checked.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel
Copy link
Copy Markdown

vercel Bot commented May 5, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped May 18, 2026 8:13pm

Request Review

@cursor
Copy link
Copy Markdown

cursor Bot commented May 5, 2026

PR Summary

High Risk
High risk because it introduces a new OAuth token storage/migration and changes MCP server lifecycle, connection management, and tool execution/discovery error handling in security-sensitive auth paths.

Overview
Adds an end-to-end OAuth 2.1 + PKCE flow for outbound MCP servers, including new /api/mcp/oauth/start and /api/mcp/oauth/callback routes and a popup-based UI hook that initiates auth, handles postMessage results, and refreshes MCP queries.

Extends MCP server CRUD and orchestration to support authType plus optional preregistered client credentials (advanced settings), stores OAuth state/tokens in a new mcp_server_oauth table (encrypted), auto-detects OAuth need via a probe, revokes/clears credentials on URL/credential changes or delete, and hides secrets in API responses via hasOauthClientSecret.

Updates MCP client/service/connection-manager to pass an SDK authProvider, serialize refreshes, share managed OAuth connections per server, and surface OAuth re-authorization as explicit 401/reauth_required responses from tool discover/execute paths.

Reviewed by Cursor Bugbot for commit 5be7f79. Configure here.

Comment thread apps/sim/lib/mcp/oauth/provider.ts
Comment thread apps/sim/hooks/queries/mcp.ts
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 5, 2026

Greptile Summary

This PR adds spec-compliant OAuth 2.1 + PKCE support for outbound MCP servers. It introduces auto-detection of OAuth requirement via an unauthenticated probe, persists per-server tokens (encrypted) in a new mcp_server_oauth table, and drives consent through a popup-based flow.

  • New mcp_server_oauth table stores encrypted OAuth artifacts (tokens, client info, PKCE verifier) per MCP server, with workspace-scoped sharing and cascade delete tied to the parent server row.
  • OAuth start/callback API routes drive the PKCE flow; state is stored as a SHA-256 hash with a dedicated stateCreatedAt timestamp independent of updatedAt, preventing token-refresh writes from extending the flow TTL.
  • SimMcpOauthProvider implements the SDK's OAuthClientProvider interface; service.ts and connection-manager.ts load an OAuth provider when authType === 'oauth' and gate connections on existing tokens.

Confidence Score: 5/5

Safe to merge; the two findings are non-blocking UI robustness concerns in the popup hook.

The OAuth flow is well-structured: state is hashed and TTL-gated on a dedicated column, tokens and PKCE verifier are encrypted, workspace ownership is verified before any mutation, and error paths surface reconnect prompts rather than generic failures. The two findings are scoped entirely to the popup hook and do not affect data integrity or the OAuth protocol.

apps/sim/hooks/mcp/use-mcp-oauth-popup.ts — popup Window reference is not retained, preventing both the event.source check and clean interval replacement on retry.

Important Files Changed

Filename Overview
apps/sim/lib/mcp/oauth/storage.ts New file implementing DB-backed OAuth storage. Tokens, clientInformation, and codeVerifier are all encrypted. State is stored as a SHA-256 hash with a dedicated stateCreatedAt column.
apps/sim/app/api/mcp/oauth/callback/route.ts New callback route completing the token exchange. State is burned before exchange to prevent replay. Workspace and user validation is thorough.
apps/sim/app/api/mcp/oauth/start/route.ts New OAuth start route; validates server ownership, checks for concurrent flows keyed by stateCreatedAt TTL, and throws McpOauthRedirectRequired to surface the authorization URL.
apps/sim/lib/mcp/oauth/provider.ts Implements OAuthClientProvider for the MCP SDK. Pre-registered client credentials short-circuit DCR. invalidateCredentials correctly maps scope to DB clears.
apps/sim/lib/mcp/orchestration/server-lifecycle.ts Extended with OAuth credential handling, auto-probe for authType, and token revocation/cleanup on URL or credential change.
apps/sim/lib/mcp/service.ts createClient branches on authType to build an OAuth provider inside withMcpOauthRefreshLock. discoverAllTools and getServerSummaries handle McpOauthAuthorizationRequiredError and UnauthorizedError by marking servers disconnected.
apps/sim/hooks/mcp/use-mcp-oauth-popup.ts New hook managing the OAuth popup lifecycle. origin is validated but event.source is not checked, and the popup Window reference is not retained for interval cleanup on retry.
apps/sim/lib/mcp/connection-manager.ts Connection key changed to serverId-only, matching workspace-shared OAuth model. OAuth row is re-fetched on each connect so reconnects pick up refreshed tokens.
packages/db/migrations/0209_mcp_oauth.sql Adds mcp_server_oauth table and auth_type/oauth columns to mcp_servers. FK cascades are correct; unique index on mcp_server_id enforces one OAuth row per server.
apps/sim/lib/mcp/oauth/probe.ts Sends a POST initialize to detect OAuth via 401+WWW-Authenticate. Fires a best-effort DELETE to clean up any session the probe created. 5-second abort timeout prevents long hangs.

Sequence Diagram

sequenceDiagram
    participant UI as Settings UI
    participant Start as /api/mcp/oauth/start
    participant DB as mcp_server_oauth (DB)
    participant AS as Authorization Server
    participant CB as /api/mcp/oauth/callback
    participant Svc as MCP Service

    UI->>Start: "GET ?serverId=X&workspaceId=W"
    Start->>DB: getOrCreateOauthRow
    DB-->>Start: row
    Start->>DB: saveState(hash(state))
    Start->>AS: mcpAuth - DCR + authorization URL
    AS-->>Start: McpOauthRedirectRequired(authorizationUrl)
    Start-->>UI: redirect + authorizationUrl
    UI->>AS: window.open(authorizationUrl) [popup]
    AS-->>CB: "GET /callback?code=X&state=Y"
    CB->>DB: loadOauthRowByState(hash(state)) + TTL
    CB->>DB: clearState(row.id)
    CB->>AS: mcpAuth(code) token exchange
    AS-->>CB: AUTHORIZED
    CB->>DB: saveTokens(encrypted), clearVerifier
    CB->>Svc: clearCache + discoverServerTools
    CB-->>UI: "postMessage mcp-oauth ok=true"
    UI->>UI: invalidate queries, toast success
Loading

Reviews (39): Last reviewed commit: "fix(mcp): revert optimistic oauthClientI..." | Re-trigger Greptile

Comment thread apps/sim/app/workspace/[workspaceId]/settings/components/mcp/mcp.tsx Outdated
Comment thread apps/sim/lib/mcp/oauth/storage.ts
Comment thread apps/sim/lib/mcp/oauth/storage.ts Outdated
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

Greptile summary findings addressed in f587e82:

  • Edit modal drops existing OAuth Client ID: editInitialData now includes oauthClientId; the API already returns it (only the secret is masked) so the field populates and Advanced auto-expands.
  • Shared OAuth mutation disables all buttons: per-server pending tracked in a local Set<string>; the spinner is scoped to the card whose flow is in progress.
  • Plaintext PKCE codeVerifier: now encrypted at rest via encryptSecret to match tokens/clientInformation.

The point about clearing a pre-registered Client ID by emptying the field is a follow-up — oauthClientId || undefined collapses an intentional clear into a no-op. Will tackle when adding TTL cleanup for abandoned OAuth sessions.

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/api/mcp/servers/[id]/route.ts Outdated
Comment thread apps/sim/app/api/mcp/servers/[id]/route.ts Outdated
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/api/mcp/servers/[id]/route.ts Outdated
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/api/mcp/servers/route.ts Outdated
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/api/mcp/tools/execute/route.ts
Comment thread apps/sim/lib/mcp/service.ts Outdated
Comment thread apps/sim/lib/mcp/service.ts Outdated
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

Comment thread apps/sim/app/api/mcp/oauth/callback/route.ts
Comment thread apps/sim/app/api/mcp/servers/route.ts Outdated
Comment thread apps/sim/lib/mcp/oauth/storage.ts
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/api/mcp/oauth/start/route.test.ts
Comment thread apps/sim/app/api/mcp/servers/[id]/route.ts Outdated
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/api/mcp/servers/[id]/route.ts Outdated
Comment thread apps/sim/hooks/queries/mcp.ts
Comment thread apps/sim/hooks/mcp/use-mcp-oauth-popup.ts
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/api/mcp/servers/[id]/route.ts Outdated
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 43902b4. Configure here.

waleedlatif1 and others added 4 commits May 17, 2026 10:21
Re-load the OAuth row inside withMcpOauthRefreshLock so concurrent
callers observe predecessor-written tokens instead of a stale snapshot
loaded before lock acquisition. Without this, the second caller's
provider held a rotated-out refresh token and the SDK tripped
invalid_grant, forcing reauthorization.

Switch isSessionError to match the SDK's typed StreamableHTTPError
(code 404/400) instead of substring-checking arbitrary error messages,
removing false positives on URLs that happen to contain those digits.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Validate callback query params via mcpOauthCallbackContract instead of
  raw searchParams.get, matching the rest of the MCP route surface.
- Drop non-RFC-7591 application_type field from dynamic client registration
  to avoid rejection by strict authorization servers.
- Collapse the pre-lock OAuth row load in createClient — the row is now
  loaded exclusively inside withMcpOauthRefreshLock, removing a redundant
  query and a stale-snapshot path.
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/api/mcp/servers/route.ts
The POST /api/mcp/servers handler omitted authType from the success
response, so useCreateMcpServer always saw data.data.authType as
undefined and never triggered the OAuth popup after creating an
OAuth-protected server. Thread authType through performCreateMcpServer
into the response so the client can decide whether to auto-start OAuth.
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/mcp/orchestration/server-lifecycle.ts
Comment thread apps/sim/lib/mcp/orchestration/server-lifecycle.ts
Comment thread apps/sim/hooks/queries/mcp.ts
…d update

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

…rver type

The response contract preprocesses null → undefined, so McpServer.oauthClientId
is string | undefined. Using null broke type checking.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

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 1 potential issue.

Fix All in Cursor

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

Reviewed by Cursor Bugbot for commit 5be7f79. Configure here.

params: PerformDeleteMcpServerParams
): Promise<PerformMcpServerResult> {
try {
await revokeMcpOauthTokens(params.serverId)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Orphaned OAuth rows on server hard delete

Medium Severity

performDeleteMcpServer calls revokeMcpOauthTokens (best-effort provider revocation) but never deletes the mcpServerOauth DB row. Since performDeleteMcpServer does a hard db.delete(mcpServers), the OAuth row becomes orphaned. Because server IDs are deterministic (hash of workspace + URL), re-adding the same URL regenerates the same ID, and getOrCreateOauthRow finds the stale orphan with revoked tokens and possibly invalid clientInformation from a prior DCR registration. This can cause confusing token-exchange failures instead of a clean re-auth prompt.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5be7f79. Configure here.

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