moadim 3.2.4

Loop engine for AI agents — routines over REST, MCP, and a built-in web UI

#[tool_router]
impl MoadimMcp {
    /// Create a new `MoadimMcp` handler connected to the given routine store.
    pub const fn new(
        routines: RoutineStore,
        routines_dir: std::path::PathBuf,
        uptime_start: u64,
        shutdown: ShutdownSignal,
    ) -> Self {
        Self {
            routines,
            routines_dir,
            uptime_start,
            shutdown,
        }
    }

    /// Return the exact prompt body a routine's run would receive, without creating a workbench
    /// or launching an agent.
    #[tool(
        description = "Preview the exact composed prompt body a routine's run would receive, without triggering a real run (no workbench, no agent launch). Includes the routine-origin disclosure because it is part of the composed prompt."
    )]
    fn preview_routine_prompt(
        &self,
        Parameters(IdInput { id }): Parameters<IdInput>,
    ) -> Result<CallToolResult, rmcp::ErrorData> {
        Ok(
            match routines::svc_get_prompt_preview(&self.routines, &id) {
                Ok(prompt) => ok(serde_json::json!({ "prompt": prompt })),
                Err(error) => err(error),
            },
        )
    }

    /// Snooze a routine's scheduled fires without disabling it or touching manual triggers.
    #[tool(
        description = "Snooze a routine's scheduled (cron) fires without disabling it. Set snoozed_until (unix seconds) to skip fires until that time, or skip_runs (count) to skip that many upcoming scheduled fires — set exactly one, or neither to clear an active snooze. Manual triggers (trigger_routine) always bypass snooze and run normally."
    )]
    fn snooze_routine(
        &self,
        Parameters(SnoozeRoutineInput {
            id,
            snoozed_until,
            skip_runs,
        }): Parameters<SnoozeRoutineInput>,
    ) -> Result<CallToolResult, rmcp::ErrorData> {
        Ok(
            match routines::svc_snooze(&self.routines, &id, snoozed_until, skip_runs) {
                Ok(routine) => ok(routine),
                Err(error) => err(error),
            },
        )
    }

    /// Pause or resume a routine's scheduled and manual firing for power saving, without touching
    /// its `enabled` state or crontab line.
    #[tool(
        description = "Set or clear a routine's power-saving state. While active, both trigger_routine and the routine's cron schedule refuse to launch it (distinctly from a disabled routine) — its enabled toggle and crontab line are untouched, so it resumes firing on its own once cleared."
    )]
    fn set_power_saving(
        &self,
        Parameters(SetPowerSavingInput { id, active }): Parameters<SetPowerSavingInput>,
    ) -> Result<CallToolResult, rmcp::ErrorData> {
        Ok(
            match routines::svc_set_power_saving(&self.routines, &id, active) {
                Ok(routine) => ok(routine),
                Err(error) => err(error),
            },
        )
    }

    /// Return the newest run log for a routine, or an error if the routine does not exist.
    #[tool(description = "Get a routine's newest run log by ID")]
    fn routine_logs(
        &self,
        Parameters(IdInput { id }): Parameters<IdInput>,
    ) -> Result<CallToolResult, rmcp::ErrorData> {
        Ok(match routines::svc_logs(&self.routines, &id) {
            Ok(logs) => ok(serde_json::json!({
                "logs": logs.content,
                "total_bytes": logs.total_bytes,
                "truncated": logs.truncated,
            })),
            Err(error) => err(error),
        })
    }
}

/// Combines this file's tool router with the split-out tools' (see the [`health`], [`shutdown`],
/// [`restart`], [`get_lock_status`], [`list_agents`], [`cleanup_workbenches`],
/// [`list_routines`], [`get_routine`], [`delete_routine`], [`create_routine`],
/// [`list_routine_runs`], [`update_routine`], [`trigger_routine`], [`create_flag`],
/// [`list_flags`], [`resolve_flag`], [`move_routine`], [`lock_routines`], and [`unlock_routines`] modules), since a
/// `#[tool_router]` block only collects the `#[tool]` methods in its own `impl`.
#[tool_handler(router = (Self::tool_router() + Self::health_tool_router() + Self::shutdown_tool_router() + Self::restart_tool_router() + Self::get_lock_status_tool_router() + Self::list_agents_tool_router() + Self::cleanup_workbenches_tool_router() + Self::list_routines_tool_router() + Self::get_routine_tool_router() + Self::delete_routine_tool_router() + Self::create_routine_tool_router() + Self::list_routine_runs_tool_router() + Self::update_routine_tool_router() + Self::move_routine_tool_router() + Self::trigger_routine_tool_router() + Self::create_flag_tool_router() + Self::list_flags_tool_router() + Self::resolve_flag_tool_router() + Self::lock_routines_tool_router() + Self::unlock_routines_tool_router()))]
impl rmcp::ServerHandler for MoadimMcp {}

#[cfg(test)]
#[path = "mcp_parity_tests.rs"]
mod mcp_parity_tests;
#[cfg(test)]
#[path = "mcp_prompt_preview_tests.rs"]
mod mcp_prompt_preview_tests;
#[cfg(test)]
#[path = "mcp_tests.rs"]
mod mcp_tests;