pub(crate) const TOOL_TRUNCATION_LIMIT: usize = 4000;
pub(crate) const BATCH_PREVIEW_LIMIT: usize = 500;
pub(crate) fn truncate_markdown(text: &str, max_chars: usize) -> String {
if text.len() > max_chars {
let at = text.floor_char_boundary(max_chars);
format!("{}\n\n... [truncated]", &text[..at])
} else {
text.to_string()
}
}
pub(crate) fn build_structured<const N: usize>(
fields: [(&'static str, serde_json::Value); N],
) -> serde_json::Map<String, serde_json::Value> {
fields
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect()
}
pub(crate) struct FetchStructuredParams<'a> {
pub url: &'a str,
pub status: u16,
pub content_type: &'a str,
pub markdown: &'a str,
pub timing_ms: f64,
pub has_diff: bool,
pub omitted_sections: usize,
pub total_sections: usize,
pub truncated: bool,
pub full_tokens: usize,
pub response_class: Option<&'a str>,
pub response_confidence: Option<f32>,
pub response_reason: Option<&'a str>,
pub thin_content_detected: bool,
}
pub(crate) fn build_fetch_structured_v2(
p: &FetchStructuredParams<'_>,
) -> serde_json::Map<String, serde_json::Value> {
let mut map = build_structured([
("url", serde_json::Value::String(p.url.to_string())),
("status", serde_json::Value::Number(p.status.into())),
(
"content_type",
serde_json::Value::String(p.content_type.to_string()),
),
("content", serde_json::Value::String(p.markdown.to_string())),
(
"timing_ms",
serde_json::Value::Number(
serde_json::Number::from_f64(p.timing_ms).unwrap_or(serde_json::Number::from(0)),
),
),
("has_diff", serde_json::Value::Bool(p.has_diff)),
]);
if p.total_sections > 0 {
map.insert(
"omitted_sections".to_string(),
serde_json::Value::Number(p.omitted_sections.into()),
);
map.insert(
"total_sections".to_string(),
serde_json::Value::Number(p.total_sections.into()),
);
}
if p.truncated {
map.insert("truncated".to_string(), serde_json::Value::Bool(true));
map.insert(
"full_tokens".to_string(),
serde_json::Value::Number(p.full_tokens.into()),
);
}
if let Some(response_class) = p.response_class {
map.insert(
"response_class".to_string(),
serde_json::Value::String(response_class.to_string()),
);
}
if let Some(response_confidence) = p.response_confidence
&& let Some(number) = serde_json::Number::from_f64(f64::from(response_confidence))
{
map.insert(
"response_confidence".to_string(),
serde_json::Value::Number(number),
);
}
if let Some(response_reason) = p.response_reason {
map.insert(
"response_reason".to_string(),
serde_json::Value::String(response_reason.to_string()),
);
}
if p.thin_content_detected {
map.insert(
"thin_content_detected".to_string(),
serde_json::Value::Bool(true),
);
}
map
}
pub(crate) const GLOBE_SVG_LIGHT: &str = concat!(
"data:image/svg+xml;base64,",
"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiI+",
"PGNpcmNsZSBjeD0iMTYiIGN5PSIxNiIgcj0iMTQiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzMzMyIgc3",
"Ryb2tlLXdpZHRoPSIxLjUiLz48ZWxsaXBzZSBjeD0iMTYiIGN5PSIxNiIgcng9IjYiIHJ5PSIxNCIg",
"ZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMzMzIiBzdHJva2Utd2lkdGg9IjEuNSIvPjxsaW5lIHgxPSIyIiB",
"5MT0iMTYiIHgyPSIzMCIgeTI9IjE2IiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMS41Ii8+PC",
"9zdmc+"
);
pub(crate) const GLOBE_SVG_DARK: &str = concat!(
"data:image/svg+xml;base64,",
"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiI+",
"PGNpcmNsZSBjeD0iMTYiIGN5PSIxNiIgcj0iMTQiIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2VlZSIgc3",
"Ryb2tlLXdpZHRoPSIxLjUiLz48ZWxsaXBzZSBjeD0iMTYiIGN5PSIxNiIgcng9IjYiIHJ5PSIxNCIg",
"ZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZWVlIiBzdHJva2Utd2lkdGg9IjEuNSIvPjxsaW5lIHgxPSIyIiB",
"5MT0iMTYiIHgyPSIzMCIgeTI9IjE2IiBzdHJva2U9IiNlZWUiIHN0cm9rZS13aWR0aD0iMS41Ii8+PC",
"9zdmc+"
);
pub(crate) fn server_icons() -> Vec<rust_mcp_sdk::schema::Icon> {
use rust_mcp_sdk::schema::{Icon, IconTheme};
vec![
Icon {
src: GLOBE_SVG_LIGHT.to_string(),
mime_type: Some("image/svg+xml".to_string()),
sizes: vec!["any".to_string()],
theme: Some(IconTheme::Light),
},
Icon {
src: GLOBE_SVG_DARK.to_string(),
mime_type: Some("image/svg+xml".to_string()),
sizes: vec!["any".to_string()],
theme: Some(IconTheme::Dark),
},
]
}