Skip to main content

teamcraft/mcp/tools/
args.rs

1//! Typed access to the arguments of a tool call.
2
3use super::*;
4
5pub(super) fn bad(message: impl Into<String>) -> AppError {
6    AppError::BadRequest(message.into())
7}
8
9/// Typed access to the `arguments` object of a tool call.
10pub struct Args<'a>(Option<&'a Map<String, Value>>);
11
12impl<'a> Args<'a> {
13    pub fn new(value: &'a Value) -> Self {
14        Args(value.as_object())
15    }
16
17    pub(super) fn get(&self, key: &str) -> Option<&'a Value> {
18        self.0.and_then(|m| m.get(key)).filter(|v| !v.is_null())
19    }
20
21    /// A trimmed, non-empty string (`None` when missing or empty).
22    pub fn opt_str(&self, key: &str) -> AppResult<Option<String>> {
23        match self.get(key) {
24            None => Ok(None),
25            Some(Value::String(s)) => {
26                let s = s.trim();
27                Ok((!s.is_empty()).then(|| s.to_string()))
28            }
29            Some(_) => Err(bad(format!("`{key}` must be a string."))),
30        }
31    }
32
33    pub fn req_str(&self, key: &str) -> AppResult<String> {
34        self.opt_str(key)?
35            .ok_or_else(|| bad(format!("`{key}` is required.")))
36    }
37
38    /// A positive count, clamped to `1..=max`; numeric strings are accepted.
39    pub fn limit(&self, key: &str, default: i64, max: i64) -> AppResult<i64> {
40        let n = match self.get(key) {
41            None => default,
42            Some(Value::Number(n)) => n
43                .as_i64()
44                .or_else(|| n.as_f64().filter(|f| f.fract() == 0.0).map(|f| f as i64))
45                .ok_or_else(|| bad(format!("`{key}` must be an integer.")))?,
46            Some(Value::String(s)) => s
47                .trim()
48                .parse()
49                .map_err(|_| bad(format!("`{key}` must be an integer.")))?,
50            Some(_) => return Err(bad(format!("`{key}` must be an integer."))),
51        };
52        Ok(n.clamp(1, max))
53    }
54
55    /// A count of zero or more (no clamping above `max`: larger is an error).
56    pub fn count(&self, key: &str, default: i64, max: i64) -> AppResult<i64> {
57        let n = match self.get(key) {
58            None => default,
59            Some(Value::Number(n)) => n
60                .as_i64()
61                .ok_or_else(|| bad(format!("`{key}` must be an integer.")))?,
62            Some(Value::String(s)) => s
63                .trim()
64                .parse()
65                .map_err(|_| bad(format!("`{key}` must be an integer.")))?,
66            Some(_) => return Err(bad(format!("`{key}` must be an integer."))),
67        };
68        if !(0..=max).contains(&n) {
69            return Err(bad(format!("`{key}` must be between 0 and {max}.")));
70        }
71        Ok(n)
72    }
73
74    pub fn flag(&self, key: &str, default: bool) -> AppResult<bool> {
75        match self.get(key) {
76            None => Ok(default),
77            Some(Value::Bool(b)) => Ok(*b),
78            Some(Value::String(s)) => match s.trim().to_ascii_lowercase().as_str() {
79                "true" | "yes" | "1" => Ok(true),
80                "false" | "no" | "0" => Ok(false),
81                _ => Err(bad(format!("`{key}` must be true or false."))),
82            },
83            Some(_) => Err(bad(format!("`{key}` must be true or false."))),
84        }
85    }
86
87    /// A list of strings; a single string counts as a list of one.
88    pub fn list(&self, key: &str, max: usize) -> AppResult<Vec<String>> {
89        let out: Vec<String> = match self.get(key) {
90            None => Vec::new(),
91            Some(Value::String(s)) => vec![s.clone()],
92            Some(Value::Array(a)) => a
93                .iter()
94                .map(|v| {
95                    v.as_str()
96                        .map(str::to_string)
97                        .ok_or_else(|| bad(format!("`{key}` must be a list of strings.")))
98                })
99                .collect::<AppResult<_>>()?,
100            Some(_) => return Err(bad(format!("`{key}` must be a list of strings."))),
101        };
102        let out: Vec<String> = out
103            .into_iter()
104            .map(|s| s.trim().to_string())
105            .filter(|s| !s.is_empty())
106            .collect();
107        if out.len() > max {
108            return Err(bad(format!("`{key}` takes at most {max} entries.")));
109        }
110        Ok(out)
111    }
112
113    pub(super) fn rule(&self) -> AppResult<Option<RevisionRule>> {
114        self.opt_str("rule")?
115            .map(|r| {
116                RevisionRule::parse(&r)
117                    .ok_or_else(|| bad("rule must be latest, released or as_released."))
118            })
119            .transpose()
120    }
121}
122
123/// `PRT-000012` or `PRT-000012/B` → number and optional revision code.
124/// Item numbers never contain a slash.
125pub fn parse_item_ref(s: &str) -> (String, Option<String>) {
126    match s.trim().split_once('/') {
127        Some((number, code)) if !code.trim().is_empty() => {
128            (number.trim().to_string(), Some(code.trim().to_string()))
129        }
130        Some((number, _)) => (number.trim().to_string(), None),
131        None => (s.trim().to_string(), None),
132    }
133}