moadim 1.7.2

Loop engine for AI agents — routines over REST, MCP, and a built-in web UI
//! MCP server handler exposing routine tools over the Model Context Protocol.

use crate::routes::http::ShutdownSignal;
use crate::routines::{self, RoutineStore};
use rmcp::{
    handler::server::wrapper::Parameters,
    model::{CallToolResult, ContentBlock},
    tool, tool_handler, tool_router,
};

#[path = "mcp_types.rs"]
mod mcp_types;
use mcp_types::{IdInput, SetPowerSavingInput, SnoozeRoutineInput};

/// The `health` tool, kept in `routes/health/mcp.rs` beside the `GET /health` HTTP handler it
/// mirrors. Its own `#[tool_router]` block is combined with this file's below.
#[path = "health/mcp.rs"]
mod health;

/// The `shutdown` tool, kept in `routes/shutdown/mcp.rs` beside the `POST /shutdown` HTTP handler
/// it mirrors. Its own `#[tool_router]` block is combined with this file's below.
#[path = "shutdown/mcp.rs"]
mod shutdown;

/// The `restart` tool, kept in `routes/restart/mcp.rs` beside the `POST /restart` HTTP handler it
/// mirrors. Its own `#[tool_router]` block is combined with this file's below.
#[path = "restart/mcp.rs"]
mod restart;

/// The `get_lock_status` tool, kept in `routes/get_lock_status/mcp.rs` beside the
/// `GET /routines/lock` HTTP handler it mirrors. Its own `#[tool_router]` block is combined with
/// this file's below.
#[path = "get_lock_status/mcp.rs"]
mod get_lock_status;

/// The `list_agents` tool, kept in `routes/list_agents/mcp.rs` beside the `GET /agents` HTTP
/// handler it mirrors. Its own `#[tool_router]` block is combined with this file's below.
#[path = "list_agents/mcp.rs"]
mod list_agents;

/// The `cleanup_workbenches` tool, kept in `routes/cleanup_workbenches/mcp.rs` beside the
/// `POST /routines/cleanup` HTTP handler it mirrors. Its own `#[tool_router]` block is combined
/// with this file's below.
#[path = "cleanup_workbenches/mcp.rs"]
mod cleanup_workbenches;

/// The `list_routines` tool, kept in `routes/list_routines/mcp.rs` beside the `GET /routines`
/// HTTP handler it mirrors. Its own `#[tool_router]` block is combined with this file's below.
#[path = "list_routines/mcp.rs"]
mod list_routines;

/// The `get_routine` tool, kept in `routes/get_routine/mcp.rs` beside the `GET /routines/{id}`
/// HTTP handler it mirrors. Its own `#[tool_router]` block is combined with this file's below.
#[path = "get_routine/mcp.rs"]
mod get_routine;

/// The `delete_routine` tool, kept in `routes/delete_routine/mcp.rs` beside the
/// `DELETE /routines/{id}` HTTP handler it mirrors. Its own `#[tool_router]` block is combined
/// with this file's below.
#[path = "delete_routine/mcp.rs"]
mod delete_routine;

/// The `create_routine` tool, kept in `routes/create_routine/mcp.rs` beside the `POST /routines`
/// HTTP handler it mirrors. Its own `#[tool_router]` block is combined with this file's below.
#[path = "create_routine/mcp.rs"]
mod create_routine;

/// The `list_routine_runs` tool, kept in `routes/list_routine_runs/mcp.rs` beside the
/// `GET /routines/{id}/runs` HTTP handler it mirrors. Its own `#[tool_router]` block is combined
/// with this file's below.
#[path = "list_routine_runs/mcp.rs"]
mod list_routine_runs;

/// The `update_routine` tool, kept in `routes/update_routine/mcp.rs` beside the
/// `PATCH /routines/{id}` HTTP handler it mirrors. Its own `#[tool_router]` block is combined
/// with this file's below.
#[path = "update_routine/mcp.rs"]
mod update_routine;

/// The `trigger_routine` tool, kept in `routes/trigger_routine/mcp.rs` beside the
/// `POST /routines/{id}/trigger` HTTP handler it mirrors. Its own `#[tool_router]` block is
/// combined with this file's below.
#[path = "trigger_routine/mcp.rs"]
mod trigger_routine;

/// The `create_flag` tool, kept in `routes/create_flag/mcp.rs` beside the
/// `POST /routines/{id}/flags` HTTP handler it mirrors. Its own `#[tool_router]` block is
/// combined with this file's below.
#[path = "create_flag/mcp.rs"]
mod create_flag;

/// The `list_flags` tool, kept in `routes/list_flags/mcp.rs` beside the
/// `GET /routines/{id}/flags` HTTP handler it mirrors. Its own `#[tool_router]` block is
/// combined with this file's below.
#[path = "list_flags/mcp.rs"]
mod list_flags;

/// The `resolve_flag` tool, kept in `routes/resolve_flag/mcp.rs` beside the
/// `DELETE /routines/{id}/flags/{filename}` HTTP handler it mirrors. Its own `#[tool_router]`
/// block is combined with this file's below.
#[path = "resolve_flag/mcp.rs"]
mod resolve_flag;

/// The `lock_routines` tool, kept in `routes/lock_routines/mcp.rs` beside the
/// `POST /routines/lock` HTTP handler it mirrors. Its own `#[tool_router]` block is combined
/// with this file's below.
#[path = "lock_routines/mcp.rs"]
mod lock_routines;

/// The `unlock_routines` tool, kept in `routes/unlock_routines/mcp.rs` beside the
/// `DELETE /routines/lock` HTTP handler it mirrors. Its own `#[tool_router]` block is combined
/// with this file's below.
#[path = "unlock_routines/mcp.rs"]
mod unlock_routines;

/// MCP server handler that exposes routine management as MCP tools.
#[derive(Clone)]
pub struct MoadimMcp {
    /// Shared routine store.
    routines: RoutineStore,
    /// On-disk directory the routine store is re-scanned from on every list/get tool call.
    /// Defaults to [`crate::paths::routines_dir`]; tests point it at a tempdir for isolation.
    routines_dir: std::path::PathBuf,
    /// Unix timestamp (seconds) recorded at server startup.
    uptime_start: u64,
    /// Notify handle that triggers a graceful server shutdown (the `shutdown` tool fires it,
    /// mirroring `POST /api/v1/shutdown` and `moadim stop`).
    shutdown: ShutdownSignal,
}

/// Wrap a serializable value in a successful `CallToolResult`.
fn ok(val: impl serde::Serialize) -> CallToolResult {
    CallToolResult::success(vec![ContentBlock::text(
        serde_json::to_string(&val).unwrap_or_default(),
    )])
}

/// Wrap an error message in a failed `CallToolResult`.
fn err(msg: impl std::fmt::Display) -> CallToolResult {
    CallToolResult::error(vec![ContentBlock::text(msg.to_string())])
}

#[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`], [`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::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;