agentmux_mcp/tools/
mod.rs1use super::*;
10
11mod accounts;
12mod browser;
13mod cron;
14mod fleet;
15mod global_memory;
16mod history;
17mod loops;
18mod memory;
19mod open_prs;
20mod overlap;
21mod panes;
22mod presets;
23mod pty_shell;
24mod shell;
25mod ui;
26mod widgets;
27mod work;
28
29#[derive(Clone, Copy)]
31pub(crate) struct ToolCtx<'a> {
32 pub(crate) local_url: &'a str,
33 pub(crate) auth_key: &'a str,
34 pub(crate) block_id: &'a str,
35 pub(crate) client: &'a reqwest::Client,
36 pub(crate) loops: &'a LoopRegistry,
37 pub(crate) loop_counter: &'a AtomicU64,
38}
39
40fn pane_arg(arguments: &Value) -> Option<String> {
43 arguments
44 .get("pane")
45 .and_then(|v| v.as_str())
46 .map(str::trim)
47 .filter(|s| !s.is_empty())
48 .map(str::to_string)
49}
50
51#[derive(Debug)]
53struct NotInFamily;
54
55impl std::fmt::Display for NotInFamily {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 f.write_str("not in this tool family")
58 }
59}
60
61impl std::error::Error for NotInFamily {}
62
63fn not_in_family() -> anyhow::Error {
64 anyhow::Error::new(NotInFamily)
65}
66
67pub(crate) async fn call(name: &str, arguments: &Value, cx: &ToolCtx<'_>) -> Result<String> {
69 macro_rules! route {
70 ($($family:ident),*) => {$(
71 match $family::call(name, arguments, cx).await {
72 Err(e) if e.is::<NotInFamily>() => {}
73 answered => return answered,
74 }
75 )*};
76 }
77 route!(
78 shell,
79 pty_shell,
80 panes,
81 fleet,
82 history,
83 overlap,
84 ui,
85 browser,
86 loops,
87 work,
88 cron,
89 memory,
90 global_memory,
91 presets,
92 accounts,
93 widgets
94 );
95 anyhow::bail!("unknown tool: {name}")
96}
97
98#[cfg(test)]
99mod tests {
100 use std::collections::BTreeMap;
101
102 const FAMILIES: &[(&str, &str)] = &[
103 ("accounts", include_str!("accounts.rs")),
104 ("browser", include_str!("browser.rs")),
105 ("cron", include_str!("cron.rs")),
106 ("fleet", include_str!("fleet.rs")),
107 ("global_memory", include_str!("global_memory.rs")),
108 ("history", include_str!("history.rs")),
109 ("loops", include_str!("loops.rs")),
110 ("memory", include_str!("memory.rs")),
111 ("overlap", include_str!("overlap.rs")),
112 ("panes", include_str!("panes.rs")),
113 ("presets", include_str!("presets.rs")),
114 ("pty_shell", include_str!("pty_shell.rs")),
115 ("shell", include_str!("shell.rs")),
116 ("ui", include_str!("ui.rs")),
117 ("widgets", include_str!("widgets.rs")),
118 ("work", include_str!("work.rs")),
119 ];
120
121 fn arm_names(src: &str) -> Vec<String> {
123 src.lines()
124 .filter_map(|l| l.strip_prefix(" \""))
125 .filter_map(|l| l.strip_suffix(" => {"))
126 .flat_map(|pat| pat.split(" | ").map(|n| n.trim_matches('"').to_string()))
127 .collect()
128 }
129
130 #[test]
134 fn every_advertised_tool_has_exactly_one_handler() {
135 let mut owner: BTreeMap<String, &str> = BTreeMap::new();
136 for (family, src) in FAMILIES {
137 let names = arm_names(src);
138 assert!(!names.is_empty(), "{family} has no arms");
139 for n in names {
140 if let Some(prev) = owner.insert(n.clone(), family) {
141 panic!("{n} is handled by both {prev} and {family}");
142 }
143 }
144 }
145 let schemas = include_str!("../tool_schemas.rs");
146 let advertised: Vec<String> = schemas
147 .lines()
148 .filter_map(|l| l.trim().strip_prefix("\"name\": \""))
149 .filter_map(|l| l.split('"').next())
150 .map(str::to_string)
151 .collect();
152 assert!(!advertised.is_empty());
153 for n in &advertised {
154 assert!(owner.contains_key(n), "{n} is advertised but has no handler");
155 }
156 for n in owner.keys() {
157 assert!(advertised.contains(n), "{n} has a handler but no schema");
158 }
159 }
160
161 #[tokio::test]
162 async fn an_unknown_tool_is_reported_by_name() {
163 let client = reqwest::Client::new();
164 let loops = super::LoopRegistry::default();
165 let counter = std::sync::atomic::AtomicU64::new(0);
166 let cx = super::ToolCtx {
167 local_url: "",
168 auth_key: "",
169 block_id: "",
170 client: &client,
171 loops: &loops,
172 loop_counter: &counter,
173 };
174 let err = super::call("NoSuchTool", &serde_json::json!({}), &cx)
175 .await
176 .unwrap_err();
177 assert_eq!(err.to_string(), "unknown tool: NoSuchTool");
178 }
179}