#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct AuthChallenge {
pub resource_metadata: Option<String>,
pub error: Option<String>,
pub error_description: Option<String>,
}
impl AuthChallenge {
#[must_use]
pub fn parse(header: &str) -> Self {
let Some(params) = header
.split_once(char::is_whitespace)
.filter(|(scheme, _)| scheme.eq_ignore_ascii_case("Bearer"))
.map(|(_, rest)| rest)
else {
return Self::default();
};
let mut challenge = Self::default();
for part in split_params(params) {
let Some((key, value)) = part.split_once('=') else {
continue;
};
let value = unquote(value);
match key.trim().to_ascii_lowercase().as_str() {
"resource_metadata" => challenge.resource_metadata = Some(value),
"error" => challenge.error = Some(value),
"error_description" => challenge.error_description = Some(value),
_ => {},
}
}
challenge
}
}
fn split_params(params: &str) -> Vec<&str> {
let mut parts = Vec::new();
let mut start = 0;
let mut in_quotes = false;
let mut escaped = false;
for (idx, ch) in params.char_indices() {
if escaped {
escaped = false;
continue;
}
match ch {
'\\' if in_quotes => escaped = true,
'"' => in_quotes = !in_quotes,
',' if !in_quotes => {
parts.push(¶ms[start..idx]);
start = idx + 1;
},
_ => {},
}
}
parts.push(¶ms[start..]);
parts
}
fn unquote(value: &str) -> String {
let trimmed = value.trim();
let Some(inner) = trimmed.strip_prefix('"').and_then(|v| v.strip_suffix('"')) else {
return trimmed.to_owned();
};
let mut out = String::with_capacity(inner.len());
let mut escaped = false;
for ch in inner.chars() {
match ch {
'\\' if !escaped => escaped = true,
_ => {
escaped = false;
out.push(ch);
},
}
}
out
}
#[derive(Debug, thiserror::Error)]
pub enum McpTransportError {
#[error("{0}")]
Http(#[from] reqwest::Error),
#[error(
"authorization required{}: {reason}",
if *enterprise_managed { " (enterprise-managed)" } else { "" }
)]
AuthorizationRequired {
reason: String,
resource: Option<String>,
metadata_url: Option<String>,
authorization_servers: Vec<String>,
enterprise_managed: bool,
},
}