Current Behavior
Cursor desktop can't complete OAuth2 against the remote MCP server (/api/experimental/mcp/http). After Dynamic Client Registration succeeds, GET /oauth2/authorize renders:
400 - Invalid Query Parameters
Query param "redirect_uri" must exactly match cursor://anysphere.cursor-mcp/oauth/callback
Cursor registers two redirect URIs in one DCR request, per their MCP docs:
cursor://anysphere.cursor-mcp/oauth/callback
https://www.cursor.com/agents/mcp/oauth/callback
Coder stores both, then only ever validates against the first one.
Root cause
coderd/oauth2provider/registration.go persists the full list but derives a single primary URL:
CallbackURL: req.RedirectURIs[0], // Primary redirect URI
RedirectUris: req.RedirectURIs,
app.RedirectUris is never read again. ShowAuthorizePage, ProcessAuthorize (authorize.go) and Tokens (tokens.go) all do:
callbackURL, err := url.Parse(app.CallbackURL)
...
params, _, err := extractAuthorizeParams(r, callbackURL)
and httpapi.QueryParamParser.RedirectURL enforces v.String() != base.String() against that single URL.
So any RFC 7591 client that registers more than one redirect_uris entry and uses anything but index 0 is rejected. This affects Cursor today and will affect any client that registers a desktop deep link plus a web callback.
Expected Behavior
redirect_uri on /oauth2/authorize and /oauth2/token is accepted if it exactly matches any entry in the app's registered redirect_uris (OAuth 2.1 exact-match, but against the whole allowlist). Apps created through the UI/API have an empty redirect_uris, so fall back to callback_url for them.
Steps to Reproduce
- Run Coder with
CODER_EXPERIMENTS=oauth2,mcp-server-http.
- In Cursor, add an MCP server with URL
https://<deployment>/api/experimental/mcp/http (or install the Coder Cursor plugin).
- Click Connect. Browser opens the Coder authorize page and renders the 400 above.
Proposed fix
In authorize.go and tokens.go, replace the single callbackURL with a lookup that matches the presented redirect_uri against app.RedirectUris (falling back to app.CallbackURL when the list is empty), and pass the matched URL into extractAuthorizeParams / extractTokenRequest so the existing exact-match check still holds. When redirect_uri is omitted, keep the current behavior of defaulting to callback_url. Add a test in authorize_test.go and tokens_test.go that registers two URIs and authorizes with the second.
Environment
- Coder version: dev.coder.com (main,
8b8c806)
- Cursor desktop, remote MCP with OAuth2 discovery
Related to #19581 (same Cursor callbacks, earlier failure at registration).
Current Behavior
Cursor desktop can't complete OAuth2 against the remote MCP server (
/api/experimental/mcp/http). After Dynamic Client Registration succeeds,GET /oauth2/authorizerenders:Cursor registers two redirect URIs in one DCR request, per their MCP docs:
cursor://anysphere.cursor-mcp/oauth/callbackhttps://www.cursor.com/agents/mcp/oauth/callbackCoder stores both, then only ever validates against the first one.
Root cause
coderd/oauth2provider/registration.gopersists the full list but derives a single primary URL:app.RedirectUrisis never read again.ShowAuthorizePage,ProcessAuthorize(authorize.go) andTokens(tokens.go) all do:and
httpapi.QueryParamParser.RedirectURLenforcesv.String() != base.String()against that single URL.So any RFC 7591 client that registers more than one
redirect_urisentry and uses anything but index 0 is rejected. This affects Cursor today and will affect any client that registers a desktop deep link plus a web callback.Expected Behavior
redirect_urion/oauth2/authorizeand/oauth2/tokenis accepted if it exactly matches any entry in the app's registeredredirect_uris(OAuth 2.1 exact-match, but against the whole allowlist). Apps created through the UI/API have an emptyredirect_uris, so fall back tocallback_urlfor them.Steps to Reproduce
CODER_EXPERIMENTS=oauth2,mcp-server-http.https://<deployment>/api/experimental/mcp/http(or install the Coder Cursor plugin).Proposed fix
In
authorize.goandtokens.go, replace the singlecallbackURLwith a lookup that matches the presentedredirect_uriagainstapp.RedirectUris(falling back toapp.CallbackURLwhen the list is empty), and pass the matched URL intoextractAuthorizeParams/extractTokenRequestso the existing exact-match check still holds. Whenredirect_uriis omitted, keep the current behavior of defaulting tocallback_url. Add a test inauthorize_test.goandtokens_test.gothat registers two URIs and authorizes with the second.Environment
8b8c806)Related to #19581 (same Cursor callbacks, earlier failure at registration).