use std::collections::BTreeSet;
use ureq::http::HeaderMap;
use super::error::Error;
use crate::mcp::rpc;
#[cfg(test)]
#[path = "protocol_header_tests.rs"]
mod protocol_header_tests;
pub(super) fn one<'a>(
headers: &'a HeaderMap,
name: &'static str,
status: u16,
code: &'static str,
message: &'static str,
) -> Result<Option<&'a str>, Error> {
let mut values = headers.get_all(name).iter();
let Some(value) = values.next() else {
return Ok(None);
};
if values.next().is_some() {
return Err(Error::new(status, code, message));
}
value
.to_str()
.map(Some)
.map_err(|_| Error::new(status, code, message))
}
pub(super) fn bearer(headers: &HeaderMap) -> Result<&str, Error> {
let refusal = || {
Error::new(
401,
"mcp_sign_in_required",
"Exactly one bearer sign-in is required",
)
};
let value = one(
headers,
"authorization",
401,
"mcp_sign_in_required",
"Exactly one bearer sign-in is required",
)?
.ok_or_else(refusal)?;
let (scheme, token) = value.split_once(' ').ok_or_else(refusal)?;
if !scheme.eq_ignore_ascii_case("Bearer")
|| token.is_empty()
|| !token
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || b"-._~+/=".contains(&byte))
{
return Err(refusal());
}
Ok(token)
}
pub(super) fn origin(origins: &BTreeSet<String>, headers: &HeaderMap) -> Result<(), Error> {
let value = one(
headers,
"origin",
403,
"mcp_http_origin_refused",
"The request origin is not explicitly permitted",
)?;
if value.is_some_and(|value| !origins.contains(value)) {
return Err(Error::new(
403,
"mcp_http_origin_refused",
"The request origin is not explicitly permitted",
));
}
Ok(())
}
pub(super) fn protocol(headers: &HeaderMap) -> Result<(), Error> {
let content_type = one(
headers,
"content-type",
415,
"mcp_http_content_type_invalid",
"The request body must be application/json",
)?;
if !content_type.is_some_and(|value| {
value
.split(';')
.next()
.is_some_and(|kind| kind.trim().eq_ignore_ascii_case("application/json"))
}) {
return Err(Error::new(
415,
"mcp_http_content_type_invalid",
"The request body must be application/json",
));
}
let accept = one(
headers,
"accept",
406,
"mcp_http_accept_invalid",
"Accept must include application/json and text/event-stream",
)?;
if !accept.is_some_and(|value| {
accepts(value, "application/json") && accepts(value, "text/event-stream")
}) {
return Err(Error::new(
406,
"mcp_http_accept_invalid",
"Accept must include application/json and text/event-stream",
));
}
let version = one(
headers,
"mcp-protocol-version",
400,
"mcp_http_protocol_version_unsupported",
"The supplied MCP protocol version is not served",
)?;
if version.is_some_and(|version| version != rpc::SUPPORTED_PROTOCOL_VERSION) {
return Err(Error::new(
400,
"mcp_http_protocol_version_unsupported",
"The supplied MCP protocol version is not served",
));
}
Ok(())
}
fn accepts(value: &str, kind: &str) -> bool {
let exact = value.split(',').any(|part| {
part.split(';')
.next()
.is_some_and(|media| media.trim().eq_ignore_ascii_case(kind))
});
value.split(',').any(|part| {
let mut fields = part.trim().split(';');
let Some(media) = fields.next() else {
return false;
};
if (exact && !media.trim().eq_ignore_ascii_case(kind)) || (!exact && media.trim() != "*/*")
{
return false;
}
let mut quality = None;
for field in fields {
let Some((key, value)) = field.trim().split_once('=') else {
return false;
};
if key.trim().eq_ignore_ascii_case("q") {
if quality.is_some() {
return false;
}
let Some(parsed) = parse_quality(value.trim()) else {
return false;
};
quality = Some(parsed);
}
}
quality.is_none_or(|quality| quality > 0)
})
}
fn parse_quality(value: &str) -> Option<u16> {
let (whole, fractional) = value.split_once('.').unwrap_or((value, ""));
if !matches!(whole, "0" | "1")
|| fractional.len() > 3
|| !fractional.bytes().all(|byte| byte.is_ascii_digit())
{
return None;
}
if whole == "1" {
return fractional.bytes().all(|byte| byte == b'0').then_some(1000);
}
let number = if fractional.is_empty() {
0
} else {
fractional.parse::<u16>().ok()?
};
let scale = match fractional.len() {
0 | 3 => 1,
1 => 100,
2 => 10,
_ => return None,
};
Some(number * scale)
}