#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct DnsAidInput {
pub base_url: String,
pub mcp_path: Option<String>,
pub ttl: Option<u32>,
}
impl DnsAidInput {
#[must_use]
pub fn new(base_url: impl Into<String>, mcp_path: Option<&str>) -> Self {
Self {
base_url: base_url.into(),
mcp_path: mcp_path.map(str::to_owned),
ttl: None,
}
}
#[must_use]
pub const fn ttl(mut self, secs: u32) -> Self {
self.ttl = Some(secs);
self
}
}
#[must_use]
pub fn valid_mcp_path(path: &str) -> bool {
path.starts_with('/') && zone_safe(path) && !path.contains(['?', '#'])
}
fn zone_safe(s: &str) -> bool {
s.bytes()
.all(|b| b.is_ascii_graphic() && b != b'"' && b != b'\\')
}
#[must_use]
pub fn records(input: &DnsAidInput) -> Vec<String> {
if input
.mcp_path
.as_deref()
.is_some_and(|p| !valid_mcp_path(p))
{
return Vec::new();
}
let Ok(url) = url::Url::parse(input.base_url.trim()) else {
return Vec::new();
};
let base = url.as_str().trim_end_matches('/').to_owned();
if !zone_safe(&base) {
return Vec::new();
}
if !matches!(url.scheme(), "http" | "https")
|| !url.username().is_empty()
|| url.password().is_some()
|| url.query().is_some()
|| url.fragment().is_some()
{
return Vec::new();
}
let Some(url::Host::Domain(host)) = url.host() else {
return Vec::new();
};
let host = host.strip_suffix('.').unwrap_or(host).to_ascii_lowercase();
if host.is_empty() {
return Vec::new();
}
let alpn = if url.scheme() == "https" {
"h2"
} else {
"http/1.1"
};
let port = url.port_or_known_default().unwrap_or(443);
let ttl = input.ttl.unwrap_or(3600);
let mut lines = Vec::new();
if let Some(mcp) = &input.mcp_path {
lines.push(format!(
"_mcp._agents.{host}. {ttl} IN SVCB 1 {host}. alpn=\"mcp,{alpn}\" port={port} \
mandatory=alpn,port,key65400 key65400=\"{base}{card}\"",
card = super::documents::server_card_path(mcp)
));
}
lines.push(format!(
"_index._agents.{host}. {ttl} IN SVCB 1 {host}. alpn=\"{alpn}\" port={port}"
));
lines.push(format!(
"_catalog._agents.{host}. {ttl} IN TXT \"url={base}{}\"",
super::AI_CATALOG_PATH
));
lines
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_fully_qualified_host_has_one_root_dot() {
let lines = records(&DnsAidInput::new("https://example.com.", None));
assert!(
lines[0].starts_with("_index._agents.example.com. 3600 IN SVCB 1 example.com. "),
"{lines:?}"
);
assert!(lines.iter().all(|l| !l.contains("..")), "{lines:?}");
}
#[test]
fn an_idn_base_url_makes_ascii_records() {
let lines = records(&DnsAidInput::new("https://例え.テスト/ü", None));
assert!(!lines.is_empty(), "{lines:?}");
assert!(lines.iter().all(|l| l.is_ascii()), "{lines:?}");
assert!(
lines[0].starts_with("_index._agents.xn--r8jz45g.xn--zckzah. "),
"{lines:?}"
);
assert!(lines.iter().any(|l| l.contains("/%C3%BC/")), "{lines:?}");
}
#[test]
fn mcp_site_records() {
let lines = records(&DnsAidInput::new("https://example.com/", Some("/mcp")));
assert_eq!(
lines,
vec![
"_mcp._agents.example.com. 3600 IN SVCB 1 example.com. alpn=\"mcp,h2\" port=443 \
mandatory=alpn,port,key65400 key65400=\"https://example.com/mcp/server-card\"",
"_index._agents.example.com. 3600 IN SVCB 1 example.com. alpn=\"h2\" port=443",
"_catalog._agents.example.com. 3600 IN TXT \
\"url=https://example.com/.well-known/ai-catalog.json\"",
]
);
}
#[test]
fn content_site_records_and_ports() {
let lines = records(&DnsAidInput::new("http://App.Example.org:8080", None).ttl(300));
assert_eq!(
lines,
vec![
"_index._agents.app.example.org. 300 IN SVCB 1 app.example.org. \
alpn=\"http/1.1\" port=8080",
"_catalog._agents.app.example.org. 300 IN TXT \
\"url=http://app.example.org:8080/.well-known/ai-catalog.json\"",
]
);
assert!(records(&DnsAidInput::new("not a url", None)).is_empty());
assert!(records(&DnsAidInput::new("https://127.0.0.1", None)).is_empty());
assert!(records(&DnsAidInput::new("ftp://example.com", None)).is_empty());
assert!(records(&DnsAidInput::new("https://example.com?tenant=a", None)).is_empty());
assert!(records(&DnsAidInput::new("https://example.com#top", None)).is_empty());
assert!(records(&DnsAidInput::new("https://token@example.com", None)).is_empty());
let lines = records(&DnsAidInput::new("https://exa\nmple.com", None));
assert!(
lines[0].starts_with("_index._agents.example.com. "),
"{lines:?}"
);
let lines = records(&DnsAidInput::new("https://example.com/a\"b", None));
assert!(lines.iter().any(|l| l.contains("/a%22b/")), "{lines:?}");
assert!(lines.iter().all(|l| !l.contains("a\"b")), "{lines:?}");
}
#[test]
fn mcp_path_must_be_a_plain_absolute_path() {
for bad in [
"mcp",
"/mcp?tenant=a",
"/mcp#x",
"/m\"cp",
"/m cp",
"/mcp\n",
"",
] {
assert!(!valid_mcp_path(bad), "{bad:?}");
assert!(
records(&DnsAidInput::new("https://example.com", Some(bad))).is_empty(),
"{bad:?}"
);
}
assert!(valid_mcp_path("/mcp"));
assert!(valid_mcp_path("/api/mcp/"));
}
}