Skip to main content

sim_lib_mcp/
profile.rs

1use std::collections::BTreeSet;
2
3use crate::McpSurfaceCard;
4
5/// Allow/deny filter that decides which surface rows reach a client.
6///
7/// Names are matched with simple `*` glob patterns. A row is allowed when it
8/// matches no deny pattern and either the allow set is empty or it matches an
9/// allow pattern.
10#[derive(Clone, Debug, Default, PartialEq, Eq)]
11pub struct McpProfile {
12    allow_names: BTreeSet<String>,
13    deny_names: BTreeSet<String>,
14}
15
16impl McpProfile {
17    /// Returns a profile that allows every row.
18    pub fn all() -> Self {
19        Self::default()
20    }
21
22    /// Builds a profile whose allow set is `names` and whose deny set is empty.
23    pub fn allow_names<I, S>(names: I) -> Self
24    where
25        I: IntoIterator<Item = S>,
26        S: Into<String>,
27    {
28        Self {
29            allow_names: names.into_iter().map(Into::into).collect(),
30            deny_names: BTreeSet::new(),
31        }
32    }
33
34    /// Builds a profile whose deny set is `names` and whose allow set is empty.
35    pub fn deny_names<I, S>(names: I) -> Self
36    where
37        I: IntoIterator<Item = S>,
38        S: Into<String>,
39    {
40        Self {
41            allow_names: BTreeSet::new(),
42            deny_names: names.into_iter().map(Into::into).collect(),
43        }
44    }
45
46    /// Returns the profile with `name` added to the allow set.
47    pub fn with_allowed_name(mut self, name: impl Into<String>) -> Self {
48        self.allow_names.insert(name.into());
49        self
50    }
51
52    /// Returns the profile with `name` added to the deny set.
53    pub fn with_denied_name(mut self, name: impl Into<String>) -> Self {
54        self.deny_names.insert(name.into());
55        self
56    }
57
58    /// Reports whether a row named `name` passes this profile.
59    pub fn allows_name(&self, name: &str) -> bool {
60        if matches_any(&self.deny_names, name) {
61            return false;
62        }
63        self.allow_names.is_empty() || matches_any(&self.allow_names, name)
64    }
65
66    /// Reports whether `row` passes this profile.
67    pub fn allows(&self, row: &McpSurfaceCard) -> bool {
68        self.allows_name(&row.name)
69    }
70}
71
72fn matches_any(patterns: &BTreeSet<String>, name: &str) -> bool {
73    patterns.iter().any(|pattern| glob_matches(pattern, name))
74}
75
76fn glob_matches(pattern: &str, name: &str) -> bool {
77    if pattern == "*" || pattern == name {
78        return true;
79    }
80    if !pattern.contains('*') {
81        return false;
82    }
83    let mut rest = name;
84    let anchored_start = !pattern.starts_with('*');
85    let anchored_end = !pattern.ends_with('*');
86    let parts = pattern.split('*').filter(|part| !part.is_empty());
87    let mut first = true;
88    for part in parts {
89        if first && anchored_start {
90            let Some(tail) = rest.strip_prefix(part) else {
91                return false;
92            };
93            rest = tail;
94        } else {
95            let Some(index) = rest.find(part) else {
96                return false;
97            };
98            rest = &rest[index + part.len()..];
99        }
100        first = false;
101    }
102    !anchored_end || rest.is_empty()
103}