use std::borrow::Cow;
use vtcode_commons::utils::calculate_sha256;
pub const MCP_QUALIFIED_TOOL_PREFIX: &str = "mcp__";
const MCP_TOOL_NAME_MAX_LEN: usize = 64;
const MCP_HASH_SUFFIX_LEN: usize = 8;
pub fn is_legacy_mcp_tool_name(name: &str) -> bool {
name.starts_with("mcp_") && !name.starts_with(MCP_QUALIFIED_TOOL_PREFIX)
}
pub fn legacy_mcp_tool_name(name: &str) -> Option<&str> {
if is_legacy_mcp_tool_name(name) {
name.strip_prefix("mcp_")
} else {
None
}
}
pub fn parse_canonical_mcp_tool_name(name: &str) -> Option<(&str, &str)> {
let mut parts = name.splitn(3, "::");
match (parts.next()?, parts.next(), parts.next()) {
("mcp", Some(provider), Some(tool)) if !provider.is_empty() && !tool.is_empty() => Some((provider, tool)),
_ => None,
}
}
pub fn model_visible_mcp_tool_name(provider: &str, tool_name: &str) -> String {
let provider = sanitize_tool_segment(provider);
let tool = sanitize_tool_segment(tool_name);
let qualified = format!("{MCP_QUALIFIED_TOOL_PREFIX}{provider}__{tool}");
if qualified.len() <= MCP_TOOL_NAME_MAX_LEN {
return qualified;
}
let hash = calculate_sha256(qualified.as_bytes());
let hash = &hash[..MCP_HASH_SUFFIX_LEN];
let keep = MCP_TOOL_NAME_MAX_LEN.saturating_sub(1 + MCP_HASH_SUFFIX_LEN);
let prefix = &qualified[..keep];
format!("{prefix}_{hash}")
}
fn sanitize_tool_segment(input: &str) -> Cow<'_, str> {
if input.is_empty() {
return Cow::Borrowed("tool");
}
let first_bad = input.bytes().position(|b| !b.is_ascii_alphanumeric() && b != b'_' && b != b'-');
match first_bad {
None => Cow::Borrowed(input),
Some(pos) => {
let mut out = String::with_capacity(input.len());
out.push_str(&input[..pos]);
for ch in input[pos..].chars() {
if ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' {
out.push(ch);
} else {
out.push('_');
}
}
Cow::Owned(out)
}
}
}
pub const fn canonical_tool_name(name: &str) -> &str {
name
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_canonical_mcp_tool_name_valid() {
assert_eq!(parse_canonical_mcp_tool_name("mcp::github::list_issues"), Some(("github", "list_issues")));
}
#[test]
fn parse_canonical_mcp_tool_name_invalid() {
assert_eq!(parse_canonical_mcp_tool_name("read_file"), None);
assert_eq!(parse_canonical_mcp_tool_name("mcp::"), None);
assert_eq!(parse_canonical_mcp_tool_name("mcp::provider"), None);
}
#[test]
fn legacy_mcp_tool_name_basic() {
assert_eq!(legacy_mcp_tool_name("mcp_github_list_issues"), Some("github_list_issues"));
assert_eq!(legacy_mcp_tool_name("read_file"), None);
}
#[test]
fn is_legacy_mcp_tool_name_detects_prefix() {
assert!(is_legacy_mcp_tool_name("mcp_github_list"));
assert!(!is_legacy_mcp_tool_name("mcp__github__list"));
assert!(!is_legacy_mcp_tool_name("read_file"));
}
#[test]
fn canonical_tool_name_is_identity() {
assert_eq!(canonical_tool_name("list_files"), "list_files");
assert_eq!(canonical_tool_name("unknown_tool"), "unknown_tool");
}
#[test]
fn model_visible_mcp_tool_name_short() {
let name = model_visible_mcp_tool_name("github", "list");
assert_eq!(name, "mcp__github__list");
}
#[test]
fn model_visible_mcp_tool_name_long_truncates() {
let name = model_visible_mcp_tool_name("very_long_provider_name", "very_long_tool_name_that_exceeds_limit");
assert!(name.len() <= MCP_TOOL_NAME_MAX_LEN);
assert!(name.starts_with("mcp__"));
}
}