pub const MAX_CLEAN_TOOL_NAME_LEN: usize = 64;
pub fn fence_delimiter_line(line: &str, open_char: Option<char>) -> Option<(char, bool)> {
let trimmed = line.trim();
let mut chars = trimmed.chars();
let first = chars.next()?;
if first != '`' && first != '~' {
return None;
}
let mut count = 1usize;
let mut closed_run = false;
for ch in chars {
if ch == first {
count += 1;
} else {
closed_run = true;
break;
}
}
if count < 3 {
return None;
}
if closed_run {
return Some((first, false));
}
let only_whitespace = trimmed.chars().skip(count).all(char::is_whitespace);
match open_char {
Some(open) if open == first && only_whitespace => Some((first, true)),
Some(_) => None,
None => Some((first, false)),
}
}
pub fn unfenced_byte_ranges(text: &str) -> Vec<std::ops::Range<usize>> {
let mut ranges: Vec<std::ops::Range<usize>> = Vec::new();
let mut open_char: Option<char> = None;
let mut segment_start = 0usize;
let mut cursor = 0usize;
for line in text.split_inclusive('\n') {
let line_start = cursor;
cursor += line.len();
let Some((fence_char, is_closing)) = fence_delimiter_line(line, open_char) else {
continue;
};
if is_closing {
open_char = None;
} else if open_char.is_none() {
ranges.push(segment_start..line_start);
open_char = Some(fence_char);
}
segment_start = cursor;
}
ranges.push(segment_start..text.len());
ranges.retain(|range| range.start < range.end);
ranges
}
pub fn find_unfenced_from(text: &str, needle: &str, from: usize) -> Option<usize> {
unfenced_byte_ranges(text).into_iter().find_map(|range| {
let start = range.start.max(from);
if start >= range.end {
return None;
}
text.get(start..range.end)
.and_then(|slice| slice.find(needle))
.map(|index| start + index)
})
}
pub fn is_clean_tool_name(raw: &str) -> bool {
let trimmed = raw.trim();
if trimmed.is_empty() || trimmed.len() > MAX_CLEAN_TOOL_NAME_LEN {
return false;
}
let mut chars = trimmed.chars();
let Some(first) = chars.next() else {
return false;
};
if !first.is_ascii_alphabetic() {
return false;
}
chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
}
pub fn is_dispatchable_tool_name(raw: &str) -> bool {
let trimmed = raw.trim();
if trimmed.is_empty() || trimmed.len() > MAX_CLEAN_TOOL_NAME_LEN {
return false;
}
if trimmed
.chars()
.any(|ch| ch.is_whitespace() || matches!(ch, '`' | '\u{2014}' | '\u{2013}'))
{
return false;
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unfenced_ranges_exclude_fence_bodies() {
let text = "before\n```sh\nINSIDE\n```\nafter\n";
let ranges = unfenced_byte_ranges(text);
assert_eq!(ranges.len(), 2);
let first = ranges.first().expect("first unfenced range");
let second = ranges.get(1).expect("second unfenced range");
assert!(text.get(first.clone()).is_some_and(|slice| slice.contains("before")));
assert!(text.get(second.clone()).is_some_and(|slice| slice.contains("after")));
}
#[test]
fn find_unfenced_skips_matches_inside_fences() {
let text = "docs\n```\nNEEDLE\n```\nok NEEDLE\n";
let hit = find_unfenced_from(text, "NEEDLE", 0).expect("unfenced match");
assert_eq!(text.get(hit..hit + "NEEDLE".len()), Some("NEEDLE"));
}
#[test]
fn clean_and_dispatchable_name_rules() {
assert!(is_clean_tool_name("exec_command"));
assert!(!is_clean_tool_name("has space"));
assert!(is_dispatchable_tool_name("mcp::github::list"));
assert!(!is_dispatchable_tool_name("has space"));
}
}