use std::borrow::Cow;
use std::fmt;
use mcp_usage_core::{Method, name, validate_header};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProtocolHeaders<'a> {
pub method: Method,
pub name: Option<Cow<'a, str>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ClassificationError {
UntrustedVersion(String),
MissingMethod,
MissingName,
InvalidName(String),
}
impl fmt::Display for ClassificationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UntrustedVersion(reason) => write!(f, "untrusted MCP headers: {reason}"),
Self::MissingMethod => f.write_str("missing Mcp-Method header"),
Self::MissingName => f.write_str("missing Mcp-Name header for name-bearing method"),
Self::InvalidName(reason) => write!(f, "invalid Mcp-Name header: {reason}"),
}
}
}
impl std::error::Error for ClassificationError {}
pub fn classify_protocol_headers<'a>(
version_header: Option<&str>,
method_header: Option<&str>,
name_header: Option<&'a str>,
) -> Result<ProtocolHeaders<'a>, ClassificationError> {
if let Err(reason) = validate_header(version_header) {
return Err(ClassificationError::UntrustedVersion(reason.to_string()));
}
let method = Method::parse(method_header.ok_or(ClassificationError::MissingMethod)?);
if method.carries_name() && name_header.is_none() {
return Err(ClassificationError::MissingName);
}
let name = name_header
.map(name::decode)
.transpose()
.map_err(|error| ClassificationError::InvalidName(error.to_string()))?;
Ok(ProtocolHeaders { method, name })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn common_path_borrows_the_name() {
let classified =
classify_protocol_headers(Some("2026-07-28"), Some("tools/call"), Some("weather"))
.unwrap();
assert_eq!(classified.method, Method::ToolsCall);
assert!(matches!(classified.name, Some(Cow::Borrowed("weather"))));
}
#[test]
fn legacy_requests_are_rejected_before_headers_are_trusted() {
let error =
classify_protocol_headers(Some("2025-11-25"), Some("tools/call"), Some("cheap_tool"))
.unwrap_err();
assert!(matches!(error, ClassificationError::UntrustedVersion(_)));
}
#[test]
fn name_is_required_only_where_the_spec_requires_it() {
assert!(matches!(
classify_protocol_headers(Some("2026-07-28"), Some("tools/call"), None),
Err(ClassificationError::MissingName)
));
assert!(classify_protocol_headers(Some("2026-07-28"), Some("tools/list"), None).is_ok());
}
#[test]
fn header_hot_path_has_no_body_dependency() {
let invalid_body = b"this is not JSON";
let classified =
classify_protocol_headers(Some("2026-07-28"), Some("tools/call"), Some("weather"))
.unwrap();
assert_eq!(classified.method, Method::ToolsCall);
assert_eq!(invalid_body, b"this is not JSON");
}
}