use crate::auth::{AuthorizationServerMetadata, OauthProtectedResourceMetadata};
#[derive(Debug, Clone)]
pub struct OauthServerInfo {
pub authorization_server_url: String,
pub authorization_server_metadata: AuthorizationServerMetadata,
pub resource_metadata: Option<OauthProtectedResourceMetadata>,
}
pub(crate) async fn try_fetch_metadata(
http_client: &reqwest::Client,
url: &str,
) -> Option<AuthorizationServerMetadata> {
let resp = http_client.get(url).send().await.ok()?;
if !resp.status().is_success() {
return None;
}
resp.json().await.ok()
}
pub async fn fetch_protected_resource_metadata(
http_client: &reqwest::Client,
prm_url: &str,
) -> Option<OauthProtectedResourceMetadata> {
let resp = http_client.get(prm_url).send().await.ok()?;
if !resp.status().is_success() {
return None;
}
resp.json::<OauthProtectedResourceMetadata>().await.ok()
}
pub async fn discover_protected_resource_metadata(
http_client: &reqwest::Client,
mcp_server_url: &str,
explicit_prm_url: Option<&str>,
) -> Option<OauthProtectedResourceMetadata> {
for candidate in prm_url_candidates(mcp_server_url, explicit_prm_url) {
if let Some(prm) = fetch_protected_resource_metadata(http_client, &candidate).await {
return Some(prm);
}
}
None
}
pub async fn discover_oauth_server_info(
http_client: &reqwest::Client,
mcp_server_url: &str,
explicit_prm_url: Option<&str>,
) -> Option<OauthServerInfo> {
let resource_metadata =
discover_protected_resource_metadata(http_client, mcp_server_url, explicit_prm_url).await;
if let Some(prm) = resource_metadata.as_ref() {
for auth_server in &prm.authorization_servers {
let auth_url = auth_server.as_str().trim_end_matches('/').to_string();
for url in metadata_url_fallbacks(&auth_url) {
if let Some(meta) = try_fetch_metadata(http_client, &url).await {
return Some(OauthServerInfo {
authorization_server_url: auth_url,
authorization_server_metadata: meta,
resource_metadata: resource_metadata.clone(),
});
}
}
}
}
for url in metadata_url_fallbacks(mcp_server_url) {
if let Some(meta) = try_fetch_metadata(http_client, &url).await {
return Some(OauthServerInfo {
authorization_server_url: mcp_server_url.trim_end_matches('/').to_string(),
authorization_server_metadata: meta,
resource_metadata,
});
}
}
None
}
fn prm_url_candidates(mcp_server_url: &str, explicit_prm_url: Option<&str>) -> Vec<String> {
let mut urls = Vec::new();
if let Some(url) = explicit_prm_url {
let trimmed = url.trim();
if !trimmed.is_empty() {
urls.push(trimmed.to_string());
}
}
if let Ok(parsed) = url::Url::parse(mcp_server_url) {
let origin = format!("{}://{}", parsed.scheme(), parsed.authority());
let path = parsed.path().trim_end_matches('/');
if !path.is_empty() && path != "/" {
urls.push(format!(
"{}/.well-known/oauth-protected-resource{}",
origin, path
));
}
urls.push(format!("{}/.well-known/oauth-protected-resource", origin));
}
urls
}
pub fn metadata_url_fallbacks(server_url: &str) -> Vec<String> {
let mut urls = Vec::new();
let Ok(parsed) = url::Url::parse(server_url) else {
let trimmed = server_url.trim_end_matches('/');
urls.push(format!(
"{}/.well-known/oauth-authorization-server",
trimmed
));
return urls;
};
let origin = format!("{}://{}", parsed.scheme(), parsed.authority());
let path = parsed.path().trim_end_matches('/');
let has_path = !path.is_empty() && path != "/";
if has_path {
urls.push(format!(
"{}/.well-known/oauth-authorization-server{}",
origin, path
));
urls.push(format!(
"{}/.well-known/openid-configuration{}",
origin, path
));
urls.push(format!(
"{}{}/.well-known/openid-configuration",
origin, path
));
} else {
urls.push(format!("{}/.well-known/oauth-authorization-server", origin));
urls.push(format!("{}/.well-known/openid-configuration", origin));
}
urls
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fallback_path_based_prepend_style() {
let urls = metadata_url_fallbacks("https://example.com/tenant1");
assert_eq!(
urls[0],
"https://example.com/.well-known/oauth-authorization-server/tenant1"
);
assert_eq!(
urls[1],
"https://example.com/.well-known/openid-configuration/tenant1"
);
assert_eq!(
urls[2],
"https://example.com/tenant1/.well-known/openid-configuration"
);
}
#[test]
fn fallback_path_based_trailing_slash() {
let urls = metadata_url_fallbacks("https://example.com/tenant1/");
assert_eq!(
urls[0],
"https://example.com/.well-known/oauth-authorization-server/tenant1"
);
}
#[test]
fn fallback_root_path() {
let urls = metadata_url_fallbacks("https://example.com");
assert_eq!(
urls[0],
"https://example.com/.well-known/oauth-authorization-server"
);
assert_eq!(
urls[1],
"https://example.com/.well-known/openid-configuration"
);
assert_eq!(urls.len(), 2);
}
#[test]
fn fallback_root_path_with_slash() {
let urls = metadata_url_fallbacks("https://example.com/");
assert_eq!(
urls[0],
"https://example.com/.well-known/oauth-authorization-server"
);
}
#[test]
fn prm_candidates_explicit_first() {
let cands = prm_url_candidates("https://x.example/mcp", Some("https://a.b/c"));
assert_eq!(cands[0], "https://a.b/c");
assert_eq!(
cands[1],
"https://x.example/.well-known/oauth-protected-resource/mcp"
);
assert_eq!(
cands[2],
"https://x.example/.well-known/oauth-protected-resource"
);
}
#[test]
fn prm_candidates_skips_empty_explicit() {
let cands = prm_url_candidates("https://x.example/mcp", Some(""));
assert_eq!(
cands[0],
"https://x.example/.well-known/oauth-protected-resource/mcp"
);
assert_eq!(
cands[1],
"https://x.example/.well-known/oauth-protected-resource"
);
}
#[test]
fn prm_candidates_root_url() {
let cands = prm_url_candidates("https://x.example", None);
assert_eq!(cands.len(), 1);
assert_eq!(
cands[0],
"https://x.example/.well-known/oauth-protected-resource"
);
}
}