aether-mcp-servers 0.8.9

Built-in MCP tool servers for the Aether AI agent framework
Documentation
use aether_project::{PromptCatalog, PromptFile};
use std::collections::HashSet;
use std::path::Path;

use crate::workspace_paths::relative_path;

#[doc = include_str!("../docs/prompt_rule_matcher.md")]
#[derive(Debug)]
pub struct PromptRuleMatcher {
    catalog: PromptCatalog,
    activated: HashSet<String>,
}

impl PromptRuleMatcher {
    pub fn new(catalog: PromptCatalog) -> Self {
        Self { catalog, activated: HashSet::new() }
    }

    /// Returns newly-matched rules for `file_path` and marks them as activated.
    /// Subsequent calls for the same rules return an empty `Vec`.
    pub fn get_matched_rules(&mut self, root_dir: &Path, file_path: &str) -> Vec<PromptFile> {
        let matches = self.catalog.matching_rules(&relative_path(root_dir, file_path));

        let mut result = Vec::new();
        for spec in matches {
            if self.activated.insert(spec.name.clone()) {
                tracing::info!("Activating read rule '{}' triggered by read of '{}'", spec.name, file_path);
                result.push(spec.clone());
            }
        }

        result
    }
}

impl Default for PromptRuleMatcher {
    fn default() -> Self {
        Self::new(PromptCatalog::empty())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::testing::TestWorkspace;
    use aether_project::PromptCatalog;

    const RUST_RULES_SKILL: &str = "---\ndescription: Rust conventions\ntriggers:\n  read:\n    - \"**/*.rs\"\n---\n";

    fn workspace_with_rust_rules(body: &str) -> TestWorkspace {
        TestWorkspace::new().file("rust-rules/SKILL.md", format!("{RUST_RULES_SKILL}{body}\n"))
    }

    #[test]
    fn returns_matched_rules_and_deduplicates() {
        let workspace = workspace_with_rust_rules("Rust best practices.");
        let catalog = PromptCatalog::from_dirs([workspace.root()]);
        let mut state = PromptRuleMatcher::new(catalog);
        let root_dir = Path::new("/project");

        let matched = state.get_matched_rules(root_dir, "/project/src/main.rs");
        assert_eq!(matched.len(), 1);
        assert_eq!(matched[0].body, "Rust best practices.");

        let matched2 = state.get_matched_rules(root_dir, "/project/src/lib.rs");
        assert!(matched2.is_empty());
    }

    #[test]
    fn returns_empty_for_non_matching_files() {
        let workspace = workspace_with_rust_rules("Rust rules.");
        let catalog = PromptCatalog::from_dirs([workspace.root()]);
        let mut state = PromptRuleMatcher::new(catalog);
        let root_dir = Path::new("/project");

        let matched = state.get_matched_rules(root_dir, "/project/README.md");
        assert!(matched.is_empty());
    }

    #[test]
    fn from_catalog_builds_rules() {
        let workspace = workspace_with_rust_rules("Follow Rust best practices.")
            .file("commit/SKILL.md", "---\ndescription: Commit\nuser-invocable: true\n---\nCommit message.\n");

        let catalog = PromptCatalog::from_dirs([workspace.root()]);
        let mut state = PromptRuleMatcher::new(catalog);
        let root_dir = Path::new("/project");
        let matched = state.get_matched_rules(root_dir, "/project/src/main.rs");
        assert_eq!(matched.len(), 1);
        assert!(matched[0].body.contains("Follow Rust best practices"));
    }
}