#[cfg(test)]
mod tests;
use url::Url;
const ALLOWED_SCHEMES: [&str; 2] = ["http", "https"];
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum EndpointError {
#[error("the MCP server sent an empty endpoint event")]
Empty,
#[error("could not parse the MCP endpoint URL: {0}")]
Malformed(String),
#[error("unsupported MCP endpoint scheme '{scheme}': only http and https are allowed")]
UnsupportedScheme { scheme: String },
#[error("the MCP endpoint URL has no host")]
MissingHost,
#[error("the MCP endpoint URL must not embed credentials")]
CredentialsInUrl,
#[error(
"the MCP server directed requests to '{endpoint}', which is not the configured origin '{configured}'"
)]
CrossOrigin {
endpoint: String,
configured: String,
},
}
pub(crate) fn validate_stream_url(raw: &str) -> Result<Url, EndpointError> {
let url =
Url::parse(raw.trim()).map_err(|error| EndpointError::Malformed(error.to_string()))?;
check_scheme(&url)?;
check_no_credentials(&url)?;
if url.host_str().is_none() {
return Err(EndpointError::MissingHost);
}
Ok(url)
}
pub(crate) fn resolve_endpoint(stream_url: &Url, raw: &str) -> Result<Url, EndpointError> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return Err(EndpointError::Empty);
}
let endpoint = stream_url
.join(trimmed)
.map_err(|error| EndpointError::Malformed(error.to_string()))?;
check_scheme(&endpoint)?;
check_no_credentials(&endpoint)?;
check_same_origin(stream_url, &endpoint)?;
Ok(endpoint)
}
fn check_scheme(url: &Url) -> Result<(), EndpointError> {
if ALLOWED_SCHEMES.contains(&url.scheme()) {
return Ok(());
}
Err(EndpointError::UnsupportedScheme {
scheme: url.scheme().to_string(),
})
}
fn check_no_credentials(url: &Url) -> Result<(), EndpointError> {
if url.username().is_empty() && url.password().is_none() {
return Ok(());
}
Err(EndpointError::CredentialsInUrl)
}
fn check_same_origin(stream_url: &Url, endpoint: &Url) -> Result<(), EndpointError> {
let same = stream_url.scheme() == endpoint.scheme()
&& stream_url.host() == endpoint.host()
&& stream_url.port_or_known_default() == endpoint.port_or_known_default();
if same {
return Ok(());
}
Err(EndpointError::CrossOrigin {
endpoint: describe_origin(endpoint),
configured: describe_origin(stream_url),
})
}
fn describe_origin(url: &Url) -> String {
let scheme = url.scheme();
let host = url.host_str().unwrap_or("<no host>");
match url.port_or_known_default() {
Some(port) => format!("{scheme}://{host}:{port}"),
None => format!("{scheme}://{host}"),
}
}