aether-mcp-servers 0.8.6

Built-in MCP tool servers for the Aether AI agent framework
Documentation
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::tasks::task_store::{TaskStore, TaskStoreError};
use crate::tasks::types::{Task, TaskId};

/// Input for the `task_get` tool
#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct TaskGetInput {
    /// Task ID to retrieve (e.g., "at-a1b2c3d4" or "at-a1b2c3d4.1")
    pub id: String,
}

/// Output for the `task_get` tool
#[derive(Debug, Clone, Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct TaskGetOutput {
    pub task: Task,
}

/// Get a task by ID
pub fn execute_task_get(input: TaskGetInput, store: &TaskStore) -> Result<TaskGetOutput, TaskStoreError> {
    let task_id = TaskId::from(input.id.as_str());

    let task = store.get(&task_id).ok_or(TaskStoreError::NotFound { id: input.id })?;

    Ok(TaskGetOutput { task: task.clone() })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::tasks::types::{TaskStatus, TaskUpdate};
    use crate::testing::TestTaskStore;

    #[test]
    fn test_get_root_task() {
        let mut tasks = TestTaskStore::new();

        let description = TaskUpdate { description: Some("Detailed description".to_string()), ..Default::default() };
        let created = tasks.store_mut().create(None, "Research topic", description).unwrap();

        let output = execute_task_get(TaskGetInput { id: created.id.to_string() }, tasks.store()).unwrap();

        assert_eq!(output.task.id.to_string(), created.id.to_string());
        assert_eq!(output.task.title, "Research topic");
        assert_eq!(output.task.description, Some("Detailed description".to_string()));
        assert_eq!(output.task.status, TaskStatus::Pending);
    }

    #[test]
    fn test_get_subtask() {
        let mut tasks = TestTaskStore::new();

        let root = tasks.root_task("Root");
        let subtask = tasks.subtask(&root.id, "Subtask 1");

        let output = execute_task_get(TaskGetInput { id: subtask.id.to_string() }, tasks.store()).unwrap();

        assert_eq!(output.task.id.to_string(), subtask.id.to_string());
        assert_eq!(output.task.title, "Subtask 1");
        assert_eq!(output.task.parent, Some(root.id.clone()));
    }

    #[test]
    fn test_get_task_with_flat_fields() {
        let mut tasks = TestTaskStore::new();
        let task = tasks.root_task("Task to complete");

        tasks
            .store_mut()
            .update(
                &task.id,
                TaskUpdate {
                    status: Some(TaskStatus::Completed),
                    summary: Some("Found the answer".to_string()),
                    decisions: Some(vec!["Chose option A".to_string()]),
                    facts: Some(vec!["Discovered X".to_string()]),
                    next_steps: Some(vec!["Do Y next".to_string()]),
                    files_read: Some(vec!["src/main.rs".to_string()]),
                    ..Default::default()
                },
            )
            .unwrap();

        let output = execute_task_get(TaskGetInput { id: task.id.to_string() }, tasks.store()).unwrap();

        assert_eq!(output.task.status, TaskStatus::Completed);
        assert_eq!(output.task.summary, Some("Found the answer".to_string()));
        assert_eq!(output.task.decisions, vec!["Chose option A"]);
        assert_eq!(output.task.facts, vec!["Discovered X"]);
        assert_eq!(output.task.next_steps, vec!["Do Y next"]);
        assert_eq!(output.task.files_read, vec!["src/main.rs"]);
    }

    #[test]
    fn test_get_task_with_deps() {
        let mut tasks = TestTaskStore::new();

        let root = tasks.root_task("Root");
        let sub1 = tasks.subtask(&root.id, "Subtask 1");
        let sub2 = tasks.subtask(&root.id, "Subtask 2");

        tasks
            .store_mut()
            .update(&sub2.id, TaskUpdate { deps: Some(vec![sub1.id.clone()]), ..Default::default() })
            .unwrap();

        let output = execute_task_get(TaskGetInput { id: sub2.id.to_string() }, tasks.store()).unwrap();

        assert_eq!(output.task.deps, vec![sub1.id.clone()]);
    }

    #[test]
    fn test_get_nonexistent_task() {
        let tasks = TestTaskStore::new();

        let result = execute_task_get(TaskGetInput { id: "at-nonexistent".to_string() }, tasks.store());

        assert!(result.is_err());
        assert!(matches!(result.unwrap_err(), TaskStoreError::NotFound { .. }));
    }
}