feat(mcp): OAuth 2.1 + PKCE for outbound MCP servers#4441
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryHigh Risk Overview Extends MCP server CRUD and orchestration to support Updates MCP client/service/connection-manager to pass an SDK Reviewed by Cursor Bugbot for commit 5be7f79. Configure here. |
Greptile SummaryThis 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
Confidence Score: 5/5Safe 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
Sequence DiagramsequenceDiagram
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
Reviews (39): Last reviewed commit: "fix(mcp): revert optimistic oauthClientI..." | Re-trigger Greptile |
|
Greptile summary findings addressed in f587e82:
The point about clearing a pre-registered Client ID by emptying the field is a follow-up — |
|
@greptile |
|
@cursor review |
|
@greptile |
|
@cursor review |
|
@greptile |
|
@cursor review |
|
@greptile |
|
@cursor review |
|
@cursor review |
|
@greptile |
|
@greptile |
|
@cursor review |
|
@greptile |
|
@cursor review |
|
@greptile |
|
@cursor review |
|
@cursor review |
|
@greptile |
There was a problem hiding this comment.
✅ 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.
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.
|
@greptile |
|
@cursor review |
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.
|
@cursor review |
…d update Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
@greptile |
|
@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>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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) |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 5be7f79. Configure here.


Summary
OAuthClientProviderWWW-Authenticate/oauth-protected-resource)mcp_server_oauthtable; SDK refreshes automatically before expiry/api/mcp/oauth/start→/api/mcp/oauth/callback) withstateCSRF protectionreauth_requiredfrom tool execution when refresh token is invalid so the UI can prompt to reconnectType of Change
Testing
Tested manually against OAuth-protected MCP servers (Linear). Existing header-auth servers regression-checked.
Checklist