Skip to content

fix(oauth): skip invalid redirect URIs in DCR instead of failing registration - #152

Closed
crimsonsunset wants to merge 2 commits into
mcpmux:mainfrom
crimsonsunset:fix/dcr-skip-invalid-redirect-uris
Closed

fix(oauth): skip invalid redirect URIs in DCR instead of failing registration#152
crimsonsunset wants to merge 2 commits into
mcpmux:mainfrom
crimsonsunset:fix/dcr-skip-invalid-redirect-uris

Conversation

@crimsonsunset

Copy link
Copy Markdown
Contributor

Problem

validate_redirect_uris in crates/mcpmux-gateway/src/oauth/dcr.rs hard-fails the entire DCR registration if any redirect URI in the request fails validation. This blocks Cursor (and likely other MCP clients) from connecting because Cursor 3.4.20 sends three redirect URIs in a single DCR request — two valid, one invalid:

cursor://anysphere.cursor-mcp/oauth/callback   ✓ valid (custom scheme)
https://www.cursor.com/agents/mcp/oauth/callback  ✗ invalid (https non-loopback)
http://localhost:8787/callback                  ✓ valid (loopback)

Real-world log output from McpMux v0.3.0:

[DCR] Processing registration for: Cursor (redirect_uris: [..3 URIs..])
[DCR] Validated redirect_uri: cursor://anysphere.cursor-mcp/oauth/callback (loopback=false, custom_scheme=true)
[DCR] Rejected redirect_uri: https://www.cursor.com/agents/mcp/oauth/callback (must be loopback or custom scheme)
[DCR] Registration failed: invalid_redirect_uri
← 400

Cursor cannot complete OAuth, so it cannot connect to the McpMux gateway at all.

Fix

Skip invalid URIs with a warn log instead of returning an error. Only fail the registration if zero valid URIs remain after filtering.

This is what other MCP-aware OAuth implementations have already converged on:

Behavior change

Scenario Before After
All valid URIs ✓ Pass ✓ Pass
Mix of valid + invalid ✗ Fail (400) ✓ Pass, invalid skipped with warn log
All invalid URIs ✗ Fail (400) ✗ Fail (400) — no change
Empty list ✗ Fail ✗ Fail — no change

Tests

Added two new unit tests covering the mixed-validity and all-invalid cases:

  • test_mixed_valid_and_invalid_uris_pass — reproduces the exact Cursor scenario
  • test_all_invalid_uris_fail — ensures we don't accidentally accept clients with only invalid URIs

All 5 oauth::dcr tests pass locally:

test oauth::dcr::tests::test_validate_loopback_uris ... ok
test oauth::dcr::tests::test_mixed_valid_and_invalid_uris_pass ... ok
test oauth::dcr::tests::test_validate_custom_scheme_uris ... ok
test oauth::dcr::tests::test_reject_invalid_uris ... ok
test oauth::dcr::tests::test_all_invalid_uris_fail ... ok

test result: ok. 5 passed; 0 failed

Manual verification

Built locally on macOS arm64, replaced the bundled binary in /Applications/McpMux.app, and confirmed:

  1. Cursor 3.4.20 successfully registers via DCR ([DCR] Successfully registered client: Cursor (mcp_36740f70))
  2. OAuth authorization flow completes, Cursor shows status needsAuthconnected
  3. Notion MCP tools (proxied through the gateway) are visible and callable from Cursor
  4. Logs show the invalid URI being skipped, not rejecting the request:
    [DCR] Skipping invalid redirect_uri: https://www.cursor.com/agents/mcp/oauth/callback
    [DCR] Successfully registered client: Cursor (mcp_36740f70)
    ← 200 (1ms)
    

Notes

  • Signed off per DCO (-s)
  • No changes to public API
  • Stricter validation (rejecting all invalid URIs outright) would lock McpMux out of every Cursor user's workflow until Cursor changes how it constructs DCR requests — which is out of scope for this project to wait on

…re registration

Cursor and other MCP clients send a mix of valid (custom scheme + loopback)
and invalid (https non-loopback) redirect URIs in a single DCR request.
Previously, any invalid URI caused the entire registration to fail with
`invalid_redirect_uri`, preventing affected clients from connecting at all.

This change filters out invalid URIs and only fails when zero valid URIs
remain. The behavior matches what other MCP-aware OAuth implementations
have already adopted (e.g. supabase/auth#0fed91a, Python MCP SDK,
TanStack MCP).

Example: Cursor 3.4.20 sends:
  - cursor://anysphere.cursor-mcp/oauth/callback   (valid, custom scheme)
  - https://www.cursor.com/agents/mcp/oauth/callback  (invalid, https)
  - http://localhost:8787/callback                  (valid, loopback)

Before: DCR returns 400, Cursor cannot register or connect.
After: invalid URI is skipped with a warn log, valid URIs are stored,
Cursor registers successfully.

Signed-off-by: crimsonsunset <jsangio1@gmail.com>
@crimsonsunset
crimsonsunset requested a review from its-mash as a code owner May 16, 2026 21:33
Copilot AI review requested due to automatic review settings May 16, 2026 21:33

Copilot AI 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.

Pull request overview

This PR changes OAuth DCR redirect URI handling so mixed valid/invalid redirect URI registration requests can succeed instead of failing the entire registration.

Changes:

  • Updates DCR redirect URI validation to allow registration when at least one URI is valid.
  • Logs invalid redirect URIs as skipped instead of immediately returning an error.
  • Adds unit tests for mixed-validity and all-invalid redirect URI scenarios.
Comments suppressed due to low confidence (2)

crates/mcpmux-gateway/src/oauth/dcr.rs:214

  • This change only skips invalid URIs during validation; it does not remove them from the registration. process_dcr_request still merges/saves/returns the original request.redirect_uris, and the authorization endpoint later accepts any stored URI via an exact contains check, so a non-loopback HTTPS URI in a mixed request becomes a registered redirect URI despite the log saying it was skipped. The validation needs to return/use the filtered list (for both create and update paths) or invalid redirect URIs remain allowed.
        if !is_loopback && !is_custom_scheme {
            // Skip invalid URIs (e.g. https://www.cursor.com/agents/mcp/oauth/callback)
            // rather than rejecting the entire registration — clients like Cursor send a
            // mix of valid and invalid URIs and only ever use the valid ones in practice.
            warn!(
                "[DCR] Skipping invalid redirect_uri: {} (must be loopback or custom scheme)",
                uri
            );
            continue;

crates/mcpmux-gateway/src/oauth/dcr.rs:455

  • This test only checks that mixed URIs no longer fail validation, but it does not assert the new "invalid URIs are skipped" behavior through registration storage/response. Because the production path still saves and returns the original URI list, this test passes while the invalid URI remains registered; add coverage for process_dcr_request (or for a filtering helper) that verifies invalid entries are absent from the stored client and DCR response.
        let uris = vec![
            "cursor://anysphere.cursor-mcp/oauth/callback".to_string(),
            "https://www.cursor.com/agents/mcp/oauth/callback".to_string(),
            "http://localhost:8787/callback".to_string(),
        ];
        assert!(validate_redirect_uris(&uris).is_ok());

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/mcpmux-gateway/src/oauth/dcr.rs
its-mash
its-mash previously approved these changes May 16, 2026
Fixes clippy::unnecessary_sort_by lint causing CI failure.

Signed-off-by: Joe Sangiorgio <jsangio1@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@darrenhaken

Copy link
Copy Markdown

When will this be merged in? its blocking claude code for me

@its-mash

its-mash commented Jun 9, 2026

Copy link
Copy Markdown
Member

merged by #158 . Thanks @crimsonsunset

@its-mash its-mash closed this Jun 9, 2026
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.

4 participants