vtcode_core/utils/
tool_name_parsing.rs1use std::borrow::Cow;
13
14use vtcode_commons::utils::calculate_sha256;
15
16pub const MCP_QUALIFIED_TOOL_PREFIX: &str = "mcp__";
18
19const MCP_TOOL_NAME_MAX_LEN: usize = 64;
21
22const MCP_HASH_SUFFIX_LEN: usize = 8;
24
25pub fn is_legacy_mcp_tool_name(name: &str) -> bool {
29 name.starts_with("mcp_") && !name.starts_with(MCP_QUALIFIED_TOOL_PREFIX)
30}
31
32pub fn legacy_mcp_tool_name(name: &str) -> Option<&str> {
36 if is_legacy_mcp_tool_name(name) {
37 name.strip_prefix("mcp_")
38 } else {
39 None
40 }
41}
42
43pub fn parse_canonical_mcp_tool_name(name: &str) -> Option<(&str, &str)> {
47 let mut parts = name.splitn(3, "::");
48 match (parts.next()?, parts.next(), parts.next()) {
49 ("mcp", Some(provider), Some(tool)) if !provider.is_empty() && !tool.is_empty() => Some((provider, tool)),
50 _ => None,
51 }
52}
53
54pub fn model_visible_mcp_tool_name(provider: &str, tool_name: &str) -> String {
59 let provider = sanitize_tool_segment(provider);
60 let tool = sanitize_tool_segment(tool_name);
61 let qualified = format!("{MCP_QUALIFIED_TOOL_PREFIX}{provider}__{tool}");
62
63 if qualified.len() <= MCP_TOOL_NAME_MAX_LEN {
64 return qualified;
65 }
66
67 let hash = calculate_sha256(qualified.as_bytes());
68 let hash = &hash[..MCP_HASH_SUFFIX_LEN];
69 let keep = MCP_TOOL_NAME_MAX_LEN.saturating_sub(1 + MCP_HASH_SUFFIX_LEN);
70 let prefix = &qualified[..keep];
71 format!("{prefix}_{hash}")
72}
73
74fn sanitize_tool_segment(input: &str) -> Cow<'_, str> {
75 if input.is_empty() {
76 return Cow::Borrowed("tool");
77 }
78
79 let first_bad = input.bytes().position(|b| !b.is_ascii_alphanumeric() && b != b'_' && b != b'-');
80
81 match first_bad {
82 None => Cow::Borrowed(input),
83 Some(pos) => {
84 let mut out = String::with_capacity(input.len());
85 out.push_str(&input[..pos]);
86 for ch in input[pos..].chars() {
87 if ch.is_ascii_alphanumeric() || ch == '_' || ch == '-' {
88 out.push(ch);
89 } else {
90 out.push('_');
91 }
92 }
93 Cow::Owned(out)
94 }
95 }
96}
97
98pub const fn canonical_tool_name(name: &str) -> &str {
102 name
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn parse_canonical_mcp_tool_name_valid() {
111 assert_eq!(parse_canonical_mcp_tool_name("mcp::github::list_issues"), Some(("github", "list_issues")));
112 }
113
114 #[test]
115 fn parse_canonical_mcp_tool_name_invalid() {
116 assert_eq!(parse_canonical_mcp_tool_name("read_file"), None);
117 assert_eq!(parse_canonical_mcp_tool_name("mcp::"), None);
118 assert_eq!(parse_canonical_mcp_tool_name("mcp::provider"), None);
119 }
120
121 #[test]
122 fn legacy_mcp_tool_name_basic() {
123 assert_eq!(legacy_mcp_tool_name("mcp_github_list_issues"), Some("github_list_issues"));
124 assert_eq!(legacy_mcp_tool_name("read_file"), None);
125 }
126
127 #[test]
128 fn is_legacy_mcp_tool_name_detects_prefix() {
129 assert!(is_legacy_mcp_tool_name("mcp_github_list"));
130 assert!(!is_legacy_mcp_tool_name("mcp__github__list"));
131 assert!(!is_legacy_mcp_tool_name("read_file"));
132 }
133
134 #[test]
135 fn canonical_tool_name_is_identity() {
136 assert_eq!(canonical_tool_name("list_files"), "list_files");
137 assert_eq!(canonical_tool_name("unknown_tool"), "unknown_tool");
138 }
139
140 #[test]
141 fn model_visible_mcp_tool_name_short() {
142 let name = model_visible_mcp_tool_name("github", "list");
143 assert_eq!(name, "mcp__github__list");
144 }
145
146 #[test]
147 fn model_visible_mcp_tool_name_long_truncates() {
148 let name = model_visible_mcp_tool_name("very_long_provider_name", "very_long_tool_name_that_exceeds_limit");
149 assert!(name.len() <= MCP_TOOL_NAME_MAX_LEN);
150 assert!(name.starts_with("mcp__"));
151 }
152}