Plugin MCP OAuth never reaches needsAuth — Authenticate button missing, browser never opens

Where does the bug appear (feature/product)?

Cursor IDE

Describe the Bug

A GitHub marketplace plugin that ships a remote Streamable HTTP MCP server with standard OAuth (DCR + PKCE) never enters needsAuth on Cursor Desktop 3.18.9 (macOS, Glass).

The plugin installs and the MCP row appears. Configure → Local shows “Error · Unauthorized” with only Logout and Show Output. There is no Authenticate / Connect / Login control. Reload and Logout flash “connecting” for a moment, then return to the same error. The system browser never opens.

This is not a server-side OAuth failure. Against mcp.quandora.ai/quant I confirmed:

  • OAuth discovery and DCR succeed (Cursor stores a client_id under mcp-oauth-attempts)
  • mcp_oauth_redirect is logged with authUrlHost mcp.quandora.ai
  • both of Cursor’s registered redirect URIs return the real consent page

Cursor never opens that URL. Snapshots go initializing → connected → error. authState becomes expired. Logs never show needsAuth, “MCP OAuth needsAuth (v2)”, or “Dispatching runtime auth event”. They do show listOfferingsForUI Unauthorized and “Auth error detected in listOfferingsForUI”.

Root cause in 3.18.9 mcpProcessMain.js: StreamableHTTPClientTransport.start() succeeds with no HTTP, connect phase is marked complete, then the initialize POST 401 fires auth_required as a runtime event. Runtime OAuth is only armed for isLazyAuthServer() hosts — hardcoded api.1inch.com plus Google Workspace MCP hosts with unauthenticatedConnect (gmailmcp/drivemcp/calendarmcp/docsmcp/sheetsmcp/slidesmcp.googleapis.com). mcp.quandora.ai is not on that list, so handleRuntimeAuthEvent ignores the event as “ineligible”. Official OAuth MCP hosts can prompt; a third-party plugin MCP cannot.

Secondary UI bug: the Configure button cascade handles statusType === “error” (Show Output) before canAuthenticate (Authenticate), so even a brief needsAuth would be hidden behind Error · Unauthorized.

Reproducer plugin: Quandora from varsity-tech-product/quandora-plugins, server identifier plugin-quandora-quandora

Related but not the same: forum/150962 (Connect does no network — here DCR and redirect logging do run); forum/167935 (redirect URI mismatch — both of Cursor’s URIs are accepted here).

Steps to Reproduce

  1. On Cursor Desktop 3.18.9 (macOS), install the Quandora plugin from GitHub marketplace varsity-tech-product/quandora-plugins (or any plugin whose MCP is Streamable HTTP OAuth on a host that is not api.1inch.com / Google Workspace MCP).
  2. Open Customize → the plugin → Configure. Source is “Plugin quandora”, Environment is Local.
  3. Observe Local: Error · Unauthorized. Buttons are Logout and Show Output only. No Authenticate.
  4. Click Reload. Status flashes connecting, then returns to Error · Unauthorized. No browser window.
  5. Click Logout. Same flash, same error. MCP logs show OAuth credentials cleared (user_logout), immediate reconnect, 401, mcp_oauth_redirect, and a new DCR client_id. Browser still does not open.
  6. Optional: Output → MCP Logs. Confirm mcp_oauth_redirect with authUrlHost mcp.quandora.ai, then status error / authState expired, with no needsAuth transition.

Expected Behavior

After install or Logout, a 401 from a Streamable HTTP MCP server that already completed DCR should become needsAuth with an authorizationUrl.

The Configure dialog should show Authenticate (or the marketplace install card should call openExternal, as marketplaceCardMcpAuth.js already does when status is needsAuth). Clicking it should open the system browser to the authorize URL.

Logout must not be the only control, and must not reconnect in a loop without opening the browser.

Runtime auth should not be limited to a hardcoded host allowlist. Any plugin-provided Streamable HTTP MCP with valid OAuth metadata should get the same interactive flow as Google Workspace MCP.

Please do not require a duplicate ~/.cursor/mcp.json entry as a workaround; the plugin already owns quandora mcp url

Screenshots / Screen Recordings

Operating System

MacOS

Version Information

Version: 3.18.9 (Universal)
VS Code Extension API: 1.128.0
Commit: 2ba48ff3f7514cc4643c52ca9f7b3173d9b66130
Date: 2026-08-27T01:42:22.092Z
Layout: Agent Window
Build Type: Stable
Release Track: Default
Electron: 40.10.3
Chromium: 144.0.7559.236
Node.js: 24.15.0
V8: 14.4.258.32-electron.0
xterm.js: 6.1.0-beta.291
OS: Darwin arm64 24.6.0

Does this stop you from using Cursor

No - Cursor works, but with this issue

Hey @SU_YUXUAN, thanks for the detailed report and trace.

I confirmed the endpoint behavior and your main diagnosis. mcp.quandora.ai/quant currently accepts an unauthenticated initialize request, then returns 401 with a valid WWW-Authenticate challenge on tools/list. Cursor reaches OAuth discovery and DCR, but that later challenge does not transition to the interactive sign-in state for this host. The result is Error · Unauthorized, with no Authenticate action or browser flow. This matches an issue we’re tracking.

Since you control the server, the current workaround is to return 401 with the same WWW-Authenticate resource metadata challenge on the unauthenticated initialize request instead of returning 200. That should route Cursor through the initial authentication flow and expose Authenticate, without adding a duplicate mcp.json entry.

After making that change, click Logout or Reload and check whether Local changes to Needs Authentication. If it still returns to Error · Unauthorized, please share the fresh MCP log lines around initialize, MCP OAuth redirect, and the resulting status.

I’ll post here when there is an update.

Follow-up: I applied the initialize-401 workaround on our MCP server, but I still cannot load the plugin in Cursor, so I cannot confirm Needs Authentication in the UI yet. The current blocker is GitHub marketplace import in Cursor IDE 3.18.9 Glass on macOS. This looks like a Cursor import bug, not the MCP server.

Attempt 1 — full HTTPS URL (rejected immediately; UI stays on Importing…)

Plugins → + Add → From GitHub Repository, scope User:

https://github.com/varsity-tech-product/quandora-plugins-staging

aiserver.v1.DashboardService.AddMarketplace returns:

ERROR_BAD_REQUEST / invalid_argument: Invalid URL format. Expected: github.com/owner/repo or gitlab.com/namespace/repo

The Import Marketplace dialog stays on Importing… and never shows that error. Cancel is disabled while Importing. /add-plugin with the same HTTPS URL is silent for the same reason.

Attempt 2 — scheme-less URL the API asked for (never completes)

Same dialog, same User scope:

github.com/varsity-tech-product/quandora-plugins-staging

This matches the documented format. There is no second AddMarketplace success or error in logs. The dialog stays on Importing… with Cancel disabled. No timeout.

Client behavior (3.18.9 glass)

The import form sets a busy flag, disables Cancel, and only clears busy in catch. Success is supposed to close the modal. If AddMarketplace hangs, the user is stuck. ConnectError.message is often just Error, so even a fast rejection is easy to miss.

Please:

  1. Surface AddMarketplace invalid_argument in the dialog and re-enable Cancel.
  2. Accept https://github.com/owner/repo (and .git) as well as github.com/owner/repo.
  3. Put a timeout on AddMarketplace and fail visibly.

The repo itself is valid (.cursor-plugin/marketplace.json is present; a local clone works). This is separate from the MCP OAuth host-allowlist issue. Privacy Mode (Legacy) is also on in this session; other DashboardService RPCs fail with “Cloud agent is not supported in Privacy Mode (Legacy).” Unclear if that affects the AddMarketplace clone path, but GitHub import is the call that hangs; local-folder import does not use AddMarketplace.