@@ -217,16 +217,24 @@ fn redirect_uri_matches(registered: &str, requested: &str) -> bool {
217217/// 1. Loopback: http://127.0.0.1:PORT/... or http://localhost:PORT/...
218218/// 2. Custom URL schemes: cursor://, vscode://, claude://, etc.
219219///
220- /// NOT allowed:
220+ /// NOT allowed (these are filtered from the request, not hard-failed) :
221221/// - https:// URLs (except for confidential clients with proper secrets)
222222/// - http:// URLs to non-loopback addresses
223+ ///
224+ /// Invalid URIs are silently skipped rather than rejecting the entire registration.
225+ /// This is necessary because some clients (notably Cursor) send a mix of valid and
226+ /// invalid redirect URIs in a single DCR request — failing the whole registration
227+ /// would lock those clients out entirely, even though they only ever use the valid
228+ /// URIs in practice. An error is only returned if zero valid URIs remain.
223229pub fn validate_redirect_uris ( uris : & [ String ] ) -> Result < ( ) , DcrError > {
224230 if uris. is_empty ( ) {
225231 return Err ( DcrError :: invalid_redirect_uri (
226232 "At least one redirect_uri is required" ,
227233 ) ) ;
228234 }
229235
236+ let mut valid_count = 0 ;
237+
230238 for uri in uris {
231239 let is_loopback = uri. starts_with ( "http://127.0.0.1" )
232240 || uri. starts_with ( "http://localhost" )
@@ -237,20 +245,28 @@ pub fn validate_redirect_uris(uris: &[String]) -> Result<(), DcrError> {
237245 let is_custom_scheme = !uri. starts_with ( "http://" ) && !uri. starts_with ( "https://" ) ;
238246
239247 if !is_loopback && !is_custom_scheme {
248+ // Skip invalid URIs (e.g. https://www.cursor.com/agents/mcp/oauth/callback)
249+ // rather than rejecting the entire registration — clients like Cursor send a
250+ // mix of valid and invalid URIs and only ever use the valid ones in practice.
240251 warn ! (
241- "[DCR] Rejected redirect_uri: {} (must be loopback or custom scheme)" ,
252+ "[DCR] Skipping invalid redirect_uri: {} (must be loopback or custom scheme)" ,
242253 uri
243254 ) ;
244- return Err ( DcrError :: invalid_redirect_uri (
245- "Redirect URI must be loopback (http://127.0.0.1 or http://localhost) \
246- or a custom URL scheme (e.g., cursor://, vscode://)",
247- ) ) ;
255+ continue ;
248256 }
249257
250258 debug ! (
251259 "[DCR] Validated redirect_uri: {} (loopback={}, custom_scheme={})" ,
252260 uri, is_loopback, is_custom_scheme
253261 ) ;
262+ valid_count += 1 ;
263+ }
264+
265+ if valid_count == 0 {
266+ return Err ( DcrError :: invalid_redirect_uri (
267+ "No valid redirect_uris provided — must include at least one loopback \
268+ (http://127.0.0.1 or http://localhost) or custom URL scheme (e.g., cursor://, vscode://)",
269+ ) ) ;
254270 }
255271
256272 Ok ( ( ) )
@@ -457,11 +473,34 @@ mod tests {
457473
458474 #[ test]
459475 fn test_reject_invalid_uris ( ) {
460- // Invalid URIs (non-loopback http)
476+ // Invalid URIs (non-loopback http) — fail when no valid URIs remain
461477 assert ! ( validate_redirect_uris( & [ "http://example.com/callback" . to_string( ) ] ) . is_err( ) ) ;
462478 assert ! ( validate_redirect_uris( & [ "https://example.com/callback" . to_string( ) ] ) . is_err( ) ) ;
463479 }
464480
481+ #[ test]
482+ fn test_mixed_valid_and_invalid_uris_pass ( ) {
483+ // Real-world case: Cursor sends a mix of valid (custom scheme + loopback) and
484+ // invalid (https) URIs. Registration must succeed as long as at least one valid
485+ // URI is present — otherwise clients that send any non-loopback HTTPS URI cannot
486+ // register at all.
487+ let uris = vec ! [
488+ "cursor://anysphere.cursor-mcp/oauth/callback" . to_string( ) ,
489+ "https://www.cursor.com/agents/mcp/oauth/callback" . to_string( ) ,
490+ "http://localhost:8787/callback" . to_string( ) ,
491+ ] ;
492+ assert ! ( validate_redirect_uris( & uris) . is_ok( ) ) ;
493+ }
494+
495+ #[ test]
496+ fn test_all_invalid_uris_fail ( ) {
497+ let uris = vec ! [
498+ "https://www.cursor.com/agents/mcp/oauth/callback" . to_string( ) ,
499+ "http://example.com/callback" . to_string( ) ,
500+ ] ;
501+ assert ! ( validate_redirect_uris( & uris) . is_err( ) ) ;
502+ }
503+
465504 #[ test]
466505 fn loopback_ignores_port_per_rfc_8252 ( ) {
467506 // Registered with one port, requested with another — must match.
0 commit comments