use core::fmt;
use std::sync::Arc;
use thiserror::Error;
use crate::domain::Feature;
use crate::domain::feature::FeatureProfile;
use crate::domain::ir::MethodName;
use crate::domain::mcp::session_id::VISIBLE_ASCII_RANGE;
pub const MCP_METHOD_HEADER: &str = "Mcp-Method";
pub const MCP_NAME_HEADER: &str = "Mcp-Name";
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RoutingName(Arc<str>);
impl RoutingName {
pub fn new(name: &str) -> Result<Self, RoutingHeaderError> {
if name.is_empty() {
return Err(RoutingHeaderError::EmptyName);
}
if let Some((index, byte)) = name
.bytes()
.enumerate()
.find(|(_, byte)| !VISIBLE_ASCII_RANGE.contains(byte))
{
return Err(RoutingHeaderError::NonVisibleAscii { index, byte });
}
Ok(Self(Arc::from(name)))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for RoutingName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RoutingTarget {
method: MethodName,
name: RoutingName,
}
impl RoutingTarget {
#[must_use]
pub const fn new(method: MethodName, name: RoutingName) -> Self {
Self { method, name }
}
#[must_use]
pub const fn method(&self) -> &MethodName {
&self.method
}
#[must_use]
pub const fn name(&self) -> &RoutingName {
&self.name
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RoutingHeaders(RoutingTarget);
impl RoutingHeaders {
pub fn for_profile(
target: RoutingTarget,
profile: FeatureProfile,
) -> Result<Self, RoutingHeaderError> {
if !profile.allows(Feature::StandardRoutingHeaders) {
return Err(RoutingHeaderError::FeatureUnavailable {
feature: Feature::StandardRoutingHeaders,
});
}
Ok(Self(target))
}
pub fn parse<'a, I>(fields: I, profile: FeatureProfile) -> Result<Self, RoutingHeaderError>
where
I: IntoIterator<Item = (&'a str, &'a str)>,
{
if !profile.allows(Feature::StandardRoutingHeaders) {
return Err(RoutingHeaderError::FeatureUnavailable {
feature: Feature::StandardRoutingHeaders,
});
}
let mut method: Option<&str> = None;
let mut name: Option<&str> = None;
for (field, value) in fields {
if field.eq_ignore_ascii_case(MCP_METHOD_HEADER) {
set_once(&mut method, value)?;
} else if field.eq_ignore_ascii_case(MCP_NAME_HEADER) {
set_once(&mut name, value)?;
}
}
let method = method.ok_or(RoutingHeaderError::Missing {
header: MCP_METHOD_HEADER,
})?;
let name = name.ok_or(RoutingHeaderError::Missing {
header: MCP_NAME_HEADER,
})?;
Ok(Self(RoutingTarget::new(
MethodName::new(method)?,
RoutingName::new(name)?,
)))
}
#[must_use]
pub const fn target(&self) -> &RoutingTarget {
&self.0
}
pub fn fields(&self) -> impl Iterator<Item = (&'static str, &str)> {
[
(MCP_METHOD_HEADER, self.0.method.as_str()),
(MCP_NAME_HEADER, self.0.name.as_str()),
]
.into_iter()
}
}
impl fmt::Display for RoutingHeaders {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut first = true;
for (name, value) in self.fields() {
if !first {
formatter.write_str("; ")?;
}
write!(formatter, "{name}: {value}")?;
first = false;
}
Ok(())
}
}
const fn set_once<'a>(
slot: &mut Option<&'a str>,
value: &'a str,
) -> Result<(), RoutingHeaderError> {
if slot.is_some() {
return Err(RoutingHeaderError::Repeated);
}
*slot = Some(value);
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum RoutingHeaderError {
#[error("the negotiated profile does not include the {feature} feature")]
FeatureUnavailable {
feature: Feature,
},
#[error("the required {header} header is missing")]
Missing {
header: &'static str,
},
#[error("a routing header was supplied more than once, and the two values disagree")]
Repeated,
#[error("Mcp-Name is empty")]
EmptyName,
#[error(
"a routing header contains {byte:#04X} at byte {index}, which cannot be sent as an HTTP field-value"
)]
NonVisibleAscii {
index: usize,
byte: u8,
},
#[error("Mcp-Method is not a well-formed method name: {0}")]
Method(#[from] crate::domain::ir::MethodNameError),
}
#[cfg(test)]
mod tests {
use super::{
MCP_METHOD_HEADER, MCP_NAME_HEADER, RoutingHeaderError, RoutingHeaders, RoutingName,
RoutingTarget,
};
use crate::domain::Feature;
use crate::domain::feature::FeatureProfile;
use crate::domain::ir::MethodName;
fn negotiating_profile() -> FeatureProfile {
FeatureProfile::empty().with(Feature::StandardRoutingHeaders)
}
#[derive(Debug)]
enum Failure {
Routing(RoutingHeaderError),
Method(crate::domain::ir::MethodNameError),
Display,
}
impl From<RoutingHeaderError> for Failure {
fn from(error: RoutingHeaderError) -> Self {
Self::Routing(error)
}
}
impl From<crate::domain::ir::MethodNameError> for Failure {
fn from(error: crate::domain::ir::MethodNameError) -> Self {
Self::Method(error)
}
}
fn target() -> Result<RoutingTarget, Failure> {
Ok(RoutingTarget::new(
MethodName::new("tools/call")?,
RoutingName::new("weather")?,
))
}
#[test]
fn parses_a_well_formed_pair() -> Result<(), Failure> {
let fields = [
(MCP_METHOD_HEADER, "tools/call"),
(MCP_NAME_HEADER, "weather"),
];
let parsed = RoutingHeaders::parse(fields, negotiating_profile())?;
if parsed.target().method().as_str() != "tools/call" {
return Err(Failure::Routing(RoutingHeaderError::Missing {
header: MCP_METHOD_HEADER,
}));
}
if parsed.target().name().as_str() != "weather" {
return Err(Failure::Routing(RoutingHeaderError::EmptyName));
}
Ok(())
}
#[test]
fn emits_the_spelling_the_specification_fixes() -> Result<(), Failure> {
let headers = RoutingHeaders::for_profile(target()?, negotiating_profile())?;
let emitted: Vec<(&str, &str)> = headers.fields().collect();
let expected = vec![("Mcp-Method", "tools/call"), ("Mcp-Name", "weather")];
if emitted != expected {
return Err(Failure::Routing(RoutingHeaderError::Missing {
header: MCP_METHOD_HEADER,
}));
}
Ok(())
}
#[test]
fn emitted_fields_reparse() -> Result<(), Failure> {
let headers = RoutingHeaders::for_profile(target()?, negotiating_profile())?;
let wire: Vec<(String, String)> = headers
.fields()
.map(|(name, value)| (name.to_owned(), value.to_owned()))
.collect();
let borrowed: Vec<(&str, &str)> = wire
.iter()
.map(|(name, value)| (name.as_str(), value.as_str()))
.collect();
if RoutingHeaders::parse(borrowed, negotiating_profile())? != headers {
return Err(Failure::Routing(RoutingHeaderError::Repeated));
}
Ok(())
}
#[test]
fn a_conversion_keeps_the_refusal_it_was_given() -> Result<(), Failure> {
let routing: Failure = RoutingHeaderError::Repeated.into();
let method: Failure = crate::domain::ir::MethodNameError::Empty.into();
let (Failure::Routing(repeated), Failure::Method(empty)) = (routing, method) else {
return Err(Failure::Display);
};
if repeated.to_string().is_empty() || empty.to_string().is_empty() {
return Err(Failure::Display);
}
Ok(())
}
#[test]
fn accepts_header_names_in_any_case() -> Result<(), Failure> {
let fields = [("mcp-method", "tools/call"), ("MCP-NAME", "weather")];
let parsed = RoutingHeaders::parse(fields, negotiating_profile())?;
if parsed.target().name().as_str() != "weather" {
return Err(Failure::Routing(RoutingHeaderError::EmptyName));
}
Ok(())
}
#[test]
fn does_not_fold_value_case() {
let fields = [
(MCP_METHOD_HEADER, "Tools/Call"),
(MCP_NAME_HEADER, "weather"),
];
assert_eq!(
RoutingHeaders::parse(fields, FeatureProfile::all()).map(|h| h
.target()
.method()
.as_str()
.to_owned()),
Ok("Tools/Call".to_owned())
);
}
#[test]
fn refuses_when_the_profile_lacks_the_feature() -> Result<(), Failure> {
let without = FeatureProfile::empty();
let expected = RoutingHeaderError::FeatureUnavailable {
feature: Feature::StandardRoutingHeaders,
};
if RoutingHeaders::for_profile(target()?, without) != Err(expected.clone()) {
return Err(Failure::Routing(expected));
}
let fields = [
(MCP_METHOD_HEADER, "tools/call"),
(MCP_NAME_HEADER, "weather"),
];
if RoutingHeaders::parse(fields, without) != Err(expected.clone()) {
return Err(Failure::Routing(expected));
}
Ok(())
}
#[test]
fn names_the_missing_header() {
let profile = FeatureProfile::all();
assert_eq!(
RoutingHeaders::parse([(MCP_METHOD_HEADER, "tools/call")], profile),
Err(RoutingHeaderError::Missing {
header: MCP_NAME_HEADER
})
);
assert_eq!(
RoutingHeaders::parse([(MCP_NAME_HEADER, "weather")], profile),
Err(RoutingHeaderError::Missing {
header: MCP_METHOD_HEADER
})
);
}
#[test]
fn refuses_a_repeated_header() {
let fields = [
(MCP_METHOD_HEADER, "tools/call"),
(MCP_METHOD_HEADER, "resources/read"),
(MCP_NAME_HEADER, "weather"),
];
assert_eq!(
RoutingHeaders::parse(fields, FeatureProfile::all()),
Err(RoutingHeaderError::Repeated)
);
}
#[test]
fn refuses_a_non_visible_ascii_value() {
let fields = [
(MCP_METHOD_HEADER, "tools/call"),
(MCP_NAME_HEADER, "wea\nther"),
];
assert_eq!(
RoutingHeaders::parse(fields, FeatureProfile::all()),
Err(RoutingHeaderError::NonVisibleAscii {
index: 3,
byte: b'\n'
})
);
}
#[test]
fn defers_method_validation_to_the_ir() {
let fields = [(MCP_METHOD_HEADER, "tools/"), (MCP_NAME_HEADER, "weather")];
let refused = RoutingHeaders::parse(fields, FeatureProfile::all());
assert!(
matches!(refused, Err(RoutingHeaderError::Method(_))),
"{refused:?}"
);
}
#[test]
fn refuses_a_blank_target_name() {
assert_eq!(RoutingName::new(""), Err(RoutingHeaderError::EmptyName));
}
}