pub fn tool_name_to_subcommand(tool_name: &str) -> String {
let mut out = String::new();
let mut previous_was_lower_or_digit = false;
for ch in tool_name.chars() {
if ch == '_' {
out.push('-');
previous_was_lower_or_digit = false;
} else if ch.is_ascii_uppercase() {
if previous_was_lower_or_digit {
out.push('-');
}
out.push(ch.to_ascii_lowercase());
previous_was_lower_or_digit = false;
} else {
out.push(ch.to_ascii_lowercase());
previous_was_lower_or_digit = ch.is_ascii_lowercase() || ch.is_ascii_digit();
}
}
out
}
pub fn subcommand_to_tool_name(subcommand: &str) -> String {
subcommand.replace('-', "_")
}
pub fn sanitize_cli_name(name: &str) -> String {
let mut out = String::new();
let mut pending_separator: Option<char> = None;
for ch in name.to_ascii_lowercase().chars() {
if ch.is_ascii_alphanumeric() {
if let Some(separator) = pending_separator.take() {
if !out.is_empty() {
out.push(separator);
}
}
out.push(ch);
} else if ch == '_' || ch == '-' {
pending_separator = Some(match pending_separator {
Some(_) => '-',
None => ch,
});
} else {
pending_separator = Some(match pending_separator {
Some(_) => '-',
None => '-',
});
}
}
let mut sanitized = out.trim_matches(['-', '_']).to_string();
if sanitized.is_empty() {
sanitized = "mcp".to_string();
}
if sanitized.chars().next().is_some_and(|ch| ch.is_ascii_digit()) {
sanitized = format!("mcp-{sanitized}");
}
sanitized
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn subcommand_snake_to_kebab() {
assert_eq!(tool_name_to_subcommand("get_confluence_page"), "get-confluence-page");
}
#[test]
fn subcommand_single_word() {
assert_eq!(tool_name_to_subcommand("fetch"), "fetch");
}
#[test]
fn subcommand_two_word_snake() {
assert_eq!(tool_name_to_subcommand("list_resources"), "list-resources");
}
#[test]
fn subcommand_snake_with_version() {
assert_eq!(tool_name_to_subcommand("my_tool_v2"), "my-tool-v2");
}
#[test]
fn subcommand_camel_two_word() {
assert_eq!(tool_name_to_subcommand("getConfluencePage"), "get-confluence-page");
}
#[test]
fn subcommand_camel_three_word() {
assert_eq!(tool_name_to_subcommand("createJiraIssue"), "create-jira-issue");
}
#[test]
fn subcommand_all_lowercase_no_splits() {
assert_eq!(tool_name_to_subcommand("getjiraissue"), "getjiraissue");
}
#[test]
fn subcommand_snake_trailing_number() {
assert_eq!(tool_name_to_subcommand("list_resources_v2"), "list-resources-v2");
}
#[test]
fn inverse_kebab_to_snake() {
assert_eq!(subcommand_to_tool_name("get-confluence-page"), "get_confluence_page");
}
#[test]
fn inverse_single_word() {
assert_eq!(subcommand_to_tool_name("fetch"), "fetch");
}
#[test]
fn sanitize_spaces_and_special_chars() {
assert_eq!(sanitize_cli_name("My Server!"), "my-server");
}
#[test]
fn sanitize_already_valid() {
assert_eq!(sanitize_cli_name("atlassian-labs"), "atlassian-labs");
}
#[test]
fn sanitize_leading_trailing_spaces() {
assert_eq!(sanitize_cli_name(" spaces "), "spaces");
}
#[test]
fn sanitize_empty_yields_mcp() {
assert_eq!(sanitize_cli_name(""), "mcp");
}
#[test]
fn sanitize_all_invalid_yields_mcp() {
assert_eq!(sanitize_cli_name("!!!"), "mcp");
}
#[test]
fn sanitize_digit_start_gets_prefix() {
assert_eq!(sanitize_cli_name("123abc"), "mcp-123abc");
}
#[test]
fn sanitize_multiple_spaces_collapse() {
assert_eq!(sanitize_cli_name("multi spaces"), "multi-spaces");
}
#[test]
fn sanitize_single_underscore_preserved() {
assert_eq!(sanitize_cli_name("hello_world"), "hello_world");
}
#[test]
fn sanitize_consecutive_underscores_collapse() {
assert_eq!(sanitize_cli_name("hello__world"), "hello-world");
}
#[test]
fn sanitize_mixed_consecutive_separators() {
assert_eq!(sanitize_cli_name("hello_-world"), "hello-world");
}
#[test]
fn sanitize_uppercase_lowercased() {
assert_eq!(sanitize_cli_name("Hello_World"), "hello_world");
}
}