use super::mcp::MoadimMcp;
use crate::cron_jobs::{self, new_registry, AppState, CronStore};
use crate::middlewares;
use crate::routines::{self, RoutineStore};
use crate::utils::time::now_secs;
use axum::{
extract::State,
middleware,
routing::{get, post},
Json, Router,
};
use serde::{Deserialize, Serialize};
use utoipa_swagger_ui::SwaggerUi;
#[derive(Serialize, utoipa::ToSchema)]
pub struct HealthResponse {
pub status: String,
pub uptime_secs: u64,
pub running: bool,
}
#[derive(Deserialize, utoipa::ToSchema)]
pub struct EchoRequest {
pub message: String,
}
#[derive(Serialize, utoipa::ToSchema)]
pub struct EchoResponse {
pub message: String,
pub timestamp: u64,
}
#[utoipa::path(get, path = "/",
responses((status = 200, description = "Server is running", body = str)))]
pub async fn index() -> &'static str {
"Moadim server is running"
}
#[utoipa::path(get, path = "/health",
responses((status = 200, body = HealthResponse)))]
pub async fn health(State(state): State<AppState>) -> Json<HealthResponse> {
Json(HealthResponse {
status: "ok".to_string(),
uptime_secs: now_secs() - state.uptime_start,
running: true,
})
}
#[utoipa::path(post, path = "/echo",
request_body = EchoRequest,
responses((status = 200, body = EchoResponse), (status = 400, description = "Invalid body")))]
pub async fn echo(body: axum::body::Bytes) -> Result<Json<EchoResponse>, axum::http::StatusCode> {
let parsed: EchoRequest =
serde_json::from_slice(&body).map_err(|_| axum::http::StatusCode::BAD_REQUEST)?;
Ok(Json(EchoResponse {
message: parsed.message,
timestamp: now_secs(),
}))
}
pub(crate) fn build_app(store: CronStore, routines: RoutineStore) -> Router {
use rmcp::transport::streamable_http_server::{
session::local::LocalSessionManager, StreamableHttpServerConfig, StreamableHttpService,
};
let app_state = AppState {
store: store.clone(),
handlers: new_registry(),
routines: routines.clone(),
uptime_start: now_secs(),
};
let mcp_store = store.clone();
let mcp_handlers = app_state.handlers.clone();
let mcp_routines = routines.clone();
let uptime_start = app_state.uptime_start;
let mcp_service = StreamableHttpService::new(
move || {
Ok(MoadimMcp::new(
mcp_store.clone(),
mcp_handlers.clone(),
mcp_routines.clone(),
uptime_start,
))
},
LocalSessionManager::default().into(),
StreamableHttpServerConfig::default(),
);
Router::new()
.route(
"/ui",
get(|| async {
axum::response::Html(include_str!(concat!(env!("OUT_DIR"), "/index.html")))
}),
)
.route("/", get(index))
.route("/health", get(health))
.route("/echo", post(echo))
.route("/cron-jobs", get(cron_jobs::list).post(cron_jobs::create))
.route(
"/cron-jobs/{id}",
get(cron_jobs::get)
.put(cron_jobs::replace)
.patch(cron_jobs::update)
.delete(cron_jobs::delete),
)
.route("/cron-jobs/{id}/trigger", post(cron_jobs::trigger))
.route("/cron-jobs/{id}/logs", get(cron_jobs::get_logs))
.route("/routines", get(routines::list).post(routines::create))
.route(
"/routines/{id}",
get(routines::get)
.put(routines::replace)
.patch(routines::update)
.delete(routines::delete),
)
.route("/routines/{id}/trigger", post(routines::trigger))
.route("/routines/{id}/logs", get(routines::get_logs))
.nest_service("/mcp", mcp_service)
.merge({
use utoipa::OpenApi as _;
SwaggerUi::new("/docs").url("/docs/openapi.json", crate::openapi::ApiDoc::openapi())
})
.layer(middleware::from_fn(middlewares::fs_location::fs_location))
.layer(middleware::from_fn(middlewares::logger::logger))
.with_state(app_state)
}
pub async fn run_with_listener_until(
store: CronStore,
routines: RoutineStore,
listener: tokio::net::TcpListener,
shutdown: impl std::future::Future<Output = ()> + Send + 'static,
) -> anyhow::Result<()> {
let addr = listener.local_addr()?.to_string();
let spec_path = concat!(env!("CARGO_MANIFEST_DIR"), "/apis/openapi.json");
if let Err(e) = std::fs::write(spec_path, crate::openapi::ApiDoc::to_json()) {
log::warn!("could not write openapi spec: {e}");
}
let app = build_app(store, routines);
crate::utils::startup_print::print(&addr);
axum::serve(listener, app)
.with_graceful_shutdown(shutdown)
.await?;
Ok(())
}
#[cfg(test)]
#[path = "http_tests.rs"]
mod http_tests;