1pub fn sanitize_server(name: &str) -> String {
13 let mut out = String::with_capacity(name.len());
14 let mut prev_us = false;
15 for c in name.chars() {
16 if c.is_ascii_alphanumeric() || c == '-' {
17 out.push(c);
18 prev_us = false;
19 } else if !prev_us {
20 out.push('_');
21 prev_us = true;
22 }
23 }
24 out.trim_end_matches('_').to_string()
26}
27
28pub fn wire_name(server_sanitized: &str, tool: &str) -> String {
30 format!("mcp__{server_sanitized}__{tool}")
31}
32
33pub fn tool_pattern(server: &str) -> String {
37 format!("mcp__{}__*", sanitize_server(server))
38}
39
40#[cfg(test)]
41mod tests {
42 use super::*;
43
44 #[test]
45 fn sanitize_normal() {
46 assert_eq!(sanitize_server("github"), "github");
47 }
48
49 #[test]
50 fn sanitize_slash_to_underscore() {
51 assert_eq!(sanitize_server("my/server"), "my_server");
52 }
53
54 #[test]
55 fn sanitize_collapse_runs() {
56 assert_eq!(sanitize_server("my//server"), "my_server");
57 }
58
59 #[test]
60 fn sanitize_dash_preserved() {
61 assert_eq!(sanitize_server("my-server"), "my-server");
62 }
63
64 #[test]
65 fn wire_name_format() {
66 assert_eq!(wire_name("github", "merge_pr"), "mcp__github__merge_pr");
67 }
68
69 #[test]
70 fn tool_pattern_matches_wire_names_of_that_server() {
71 use crate::agent::{ToolPolicy, ToolRule, resolve_tool_policy};
72 let rules = vec![ToolRule {
73 pattern: tool_pattern("research-gateway"),
74 policy: ToolPolicy::Allow,
75 risk: None,
76 }];
77 let wn = wire_name(&sanitize_server("research-gateway"), "research_search");
79 assert_eq!(resolve_tool_policy(&rules, &wn), ToolPolicy::Allow);
80 let other = wire_name("github", "merge_pr");
82 assert_eq!(resolve_tool_policy(&rules, &other), ToolPolicy::Ask);
83 }
84}