pub mod add;
pub mod doctor;
pub mod exec;
pub mod handlers;
pub mod import;
pub mod init;
pub mod ls;
pub mod rm;
pub mod run;
pub mod status;
pub mod sync;
pub mod update;
pub const VERBS_EXPOSED: &[&str] =
&["init", "add", "rm", "ls", "status", "sync", "update", "doctor", "import", "run", "exec"];
const _: () = assert!(VERBS_EXPOSED.len() == 11);
pub const ANNOTATIONS: &[(&str, bool, bool)] = &[
("init", false, false),
("add", false, false),
("rm", false, true),
("ls", true, false),
("status", true, false),
("sync", false, false),
("update", false, false),
("doctor", true, false),
("import", false, false),
("run", false, true),
("exec", false, true),
];
const _: () = assert!(ANNOTATIONS.len() == 11);
#[cfg(test)]
pub(crate) fn list_all() -> Vec<rmcp::model::Tool> {
crate::GrexMcpServer::tool_router().list_all()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tools_list_advertises_exactly_11() {
let tools = list_all();
assert_eq!(
tools.len(),
11,
"expected 11 tools, got {}: {:?}",
tools.len(),
tools.iter().map(|t| t.name.as_ref()).collect::<Vec<_>>()
);
let names: std::collections::BTreeSet<&str> =
tools.iter().map(|t| t.name.as_ref()).collect();
let expected: std::collections::BTreeSet<&str> = VERBS_EXPOSED.iter().copied().collect();
assert_eq!(names, expected);
}
#[test]
fn every_tool_has_both_annotation_hints() {
for t in list_all() {
let a = t
.annotations
.as_ref()
.unwrap_or_else(|| panic!("tool `{}` missing annotations entirely", t.name));
assert!(a.read_only_hint.is_some(), "tool `{}` missing read_only_hint", t.name);
assert!(a.destructive_hint.is_some(), "tool `{}` missing destructive_hint", t.name);
}
}
#[test]
fn destructive_tools_are_rm_run_exec_only() {
let mut destructive: Vec<String> = list_all()
.iter()
.filter(|t| t.annotations.as_ref().and_then(|a| a.destructive_hint).unwrap_or(false))
.map(|t| t.name.to_string())
.collect();
destructive.sort();
assert_eq!(destructive, vec!["exec", "rm", "run"]);
}
#[test]
fn annotation_matrix_matches_live_router() {
use std::collections::HashMap;
let live: HashMap<String, (bool, bool)> = list_all()
.into_iter()
.map(|t| {
let a = t.annotations.expect("annotations present");
(t.name.to_string(), (a.read_only_hint.unwrap(), a.destructive_hint.unwrap()))
})
.collect();
for (verb, ro, de) in ANNOTATIONS {
let got = live
.get(*verb)
.copied()
.unwrap_or_else(|| panic!("verb `{verb}` missing from live router"));
assert_eq!(got, (*ro, *de), "annotation mismatch for `{verb}`");
}
}
#[test]
fn lists_exactly_eleven_verbs() {
assert_eq!(VERBS_EXPOSED.len(), 11);
}
#[test]
fn excludes_serve_and_teardown() {
assert!(!VERBS_EXPOSED.contains(&"serve"));
assert!(!VERBS_EXPOSED.contains(&"teardown"));
}
#[test]
fn names_are_unique() {
let mut sorted: Vec<&str> = VERBS_EXPOSED.to_vec();
sorted.sort();
sorted.dedup();
assert_eq!(sorted.len(), VERBS_EXPOSED.len());
}
}