Skip to main content

mur_common/
mcp_naming.rs

1//! MCP wire-name encoding: `mcp__<server>__<tool>`.
2//!
3//! Single source of truth for the wire-name contract shared by the agent
4//! runtime (tool registry) and mur-core (per-tool policy patterns).
5//!
6//! The LLM tool-name field must match `^[a-zA-Z0-9_-]{1,64}$`.
7//! Server names are sanitised by collapsing non-alphanumeric/dash chars into `_`.
8
9/// Sanitise an MCP server name so it's safe to embed in a wire name.
10///
11/// Collapses any run of non-`[a-zA-Z0-9-]` chars into a single `_`.
12pub 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    // Trim trailing underscore that would make the boundary look odd.
25    out.trim_end_matches('_').to_string()
26}
27
28/// Encode a server + tool name into the `mcp__<server>__<tool>` wire format.
29pub fn wire_name(server_sanitized: &str, tool: &str) -> String {
30    format!("mcp__{server_sanitized}__{tool}")
31}
32
33/// Prefix-glob `ToolRule` pattern matching every tool of `server`
34/// (sanitises first): `mcp__<server>__*`. Feed to
35/// `mur_common::agent::resolve_tool_policy` / `ToolRule.pattern`.
36pub 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        // Every tool of the server resolves to Allow…
78        let wn = wire_name(&sanitize_server("research-gateway"), "research_search");
79        assert_eq!(resolve_tool_policy(&rules, &wn), ToolPolicy::Allow);
80        // …other servers' tools stay at the default (Ask).
81        let other = wire_name("github", "merge_pr");
82        assert_eq!(resolve_tool_policy(&rules, &other), ToolPolicy::Ask);
83    }
84}