From 2ad683b7c7d944034ea1b20d84ab04c0d5c46647 Mon Sep 17 00:00:00 2001 From: David Stern Date: Wed, 1 Oct 2025 14:38:05 -0400 Subject: [PATCH 1/2] Pull in updated discover_metadata logic from upstream. --- crates/rmcp/src/transport/auth.rs | 107 +++++++++++++++++++----------- 1 file changed, 69 insertions(+), 38 deletions(-) diff --git a/crates/rmcp/src/transport/auth.rs b/crates/rmcp/src/transport/auth.rs index e1f60d5f..46b2d28a 100644 --- a/crates/rmcp/src/transport/auth.rs +++ b/crates/rmcp/src/transport/auth.rs @@ -178,6 +178,33 @@ struct AuthorizationState { } impl AuthorizationManager { + fn well_known_paths(base_path: &str, resource: &str) -> Vec { + let trimmed = base_path.trim_start_matches('/').trim_end_matches('/'); + let mut candidates = Vec::new(); + + let mut push_candidate = |candidate: String| { + if !candidates.contains(&candidate) { + candidates.push(candidate); + } + }; + + let canonical = format!("/.well-known/{resource}"); + + if trimmed.is_empty() { + push_candidate(canonical); + return candidates; + } + + // This follows the RFC 8414 recommendation for well-known URI discovery + push_candidate(format!("{canonical}/{trimmed}")); + // This is a common pattern used by some identity providers + push_candidate(format!("/{trimmed}/.well-known/{resource}")); + // The canonical path should always be the last fallback + push_candidate(canonical); + + candidates + } + /// create new auth manager with base url pub async fn new(base_url: U) -> Result { let base_url = base_url.into_url()?; @@ -206,59 +233,63 @@ impl AuthorizationManager { /// discover oauth2 metadata pub async fn discover_metadata(&self) -> Result { - async fn try_discovery( - http_client: &HttpClient, - discovery_url: Url, - ) -> Result, AuthError> { - let response = http_client + for candidate_path in + Self::well_known_paths(self.base_url.path(), "oauth-authorization-server") + { + let mut discovery_url = self.base_url.clone(); + discovery_url.set_path(&candidate_path); + debug!("discovery url: {:?}", discovery_url); + + let response = match self + .http_client .get(discovery_url) .header("MCP-Protocol-Version", "2024-11-05") .send() - .await?; - - if response.status() == StatusCode::OK { - let metadata = response - .json::() - .await - .map_err(|e| { - AuthError::MetadataError(format!("Failed to parse metadata: {}", e)) - })?; - debug!("metadata: {:?}", metadata); - return Ok(Some(metadata)); - } - - Ok(None) - } + .await + { + Ok(r) => r, + Err(e) => { + debug!("discovery request failed: {}", e); + continue; // try next candidate if request fails + } + }; - let mut discovery_url = self.base_url.clone(); + if response.status() != StatusCode::OK { + debug!("discovery returned non-200: {}", response.status()); + continue; // try next candidate if response is not OK + } - // according to the specification, the metadata should be located at - // "/.well-known/oauth-authorization-server", followed by the path of the base url - let discovery_path = format!( - "/.well-known/oauth-authorization-server{}", - self.base_url.path() - ); - discovery_url.set_path(&discovery_path); - if let Some(metadata) = try_discovery(&self.http_client, discovery_url.clone()).await? { + // parse metadata + let metadata = response + .json::() + .await + .map_err(|e| { + // Fail the discovery if we get a 200 but cannot parse the response + // This indicates a misconfiguration on the server side + AuthError::MetadataError(format!("Failed to parse metadata: {}", e)) + })?; + debug!("metadata: {:?}", metadata); return Ok(metadata); } - // many mcp servers do not follow the spec, and instead expect there to be no - // suffix added to the discovery path, so try that too - discovery_url.set_path("/.well-known/oauth-authorization-server"); - if let Some(metadata) = try_discovery(&self.http_client, discovery_url).await? { - return Ok(metadata); - } + debug!("No valid .well-known endpoint found, falling back to default endpoints"); // fallback to default endpoints let mut auth_base = self.base_url.clone(); // discard the path part, only keep scheme, host, port auth_base.set_path(""); + // Helper function to create endpoint URL + let create_endpoint = |path: &str| -> String { + let mut url = auth_base.clone(); + url.set_path(path); + url.to_string() + }; + Ok(AuthorizationMetadata { - authorization_endpoint: format!("{}/authorize", auth_base), - token_endpoint: format!("{}/token", auth_base), - registration_endpoint: format!("{}/register", auth_base), + authorization_endpoint: create_endpoint("authorize"), + token_endpoint: create_endpoint("token"), + registration_endpoint: create_endpoint("register"), issuer: None, jwks_uri: None, scopes_supported: None, From 9f1f145f73d98c249c1fbe42f1086497ed8f5dc0 Mon Sep 17 00:00:00 2001 From: David Stern Date: Wed, 1 Oct 2025 15:53:32 -0400 Subject: [PATCH 2/2] Don't fail on the first parse error. --- crates/rmcp/src/transport/auth.rs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/crates/rmcp/src/transport/auth.rs b/crates/rmcp/src/transport/auth.rs index 46b2d28a..c45a1b03 100644 --- a/crates/rmcp/src/transport/auth.rs +++ b/crates/rmcp/src/transport/auth.rs @@ -233,6 +233,7 @@ impl AuthorizationManager { /// discover oauth2 metadata pub async fn discover_metadata(&self) -> Result { + let mut parse_error = None; for candidate_path in Self::well_known_paths(self.base_url.path(), "oauth-authorization-server") { @@ -260,18 +261,26 @@ impl AuthorizationManager { } // parse metadata - let metadata = response + let Ok(metadata) = response .json::() .await - .map_err(|e| { - // Fail the discovery if we get a 200 but cannot parse the response - // This indicates a misconfiguration on the server side - AuthError::MetadataError(format!("Failed to parse metadata: {}", e)) - })?; + .inspect_err(|e| { + // Set aside the parsing error for later, but try the next candidate. + parse_error = Some(AuthError::MetadataError(format!("Failed to parse metadata: {:#}", e))); + }) else { + // If we get here, we have a 200 but cannot parse the response. Try the next candidate. + continue; + }; debug!("metadata: {:?}", metadata); return Ok(metadata); } + // If we get here, we have tried all candidates and none worked. If one returned 200 but we + // could not parse the response, return that error. + if let Some(e) = parse_error { + return Err(e); + } + debug!("No valid .well-known endpoint found, falling back to default endpoints"); // fallback to default endpoints