//! Assembles the HTTP application.
//!
//! The application exposes exactly two endpoints:
//!
//! - `/mcp`: the MCP Streamable HTTP endpoint, the only interface for business
//! operations. Domain modules add tools to it, never HTTP routes.
//! - `GET /health`: liveness/readiness probe for load balancers and
//! orchestrators, which cannot speak MCP. It exposes no business data.
use axum::extract::State;
use axum::http::StatusCode;
use axum::routing::get;
use axum::{Json, Router};
use serde::Serialize;
use crate::base::mcp;
use crate::base::state::AppState;
pub fn router(state: AppState) -> Router {
Router::new()
.route("/health", get(health))
.nest_service(mcp::PATH, mcp::service(state.clone()))
.with_state(state)
}
#[derive(Serialize)]
struct Health {
status: &'static str,
app: String,
database: &'static str,
}
/// Reports whether the app is up and its database reachable (503 if not).
async fn health(State(state): State<AppState>) -> (StatusCode, Json<Health>) {
// `query`, not `execute`: Turso rejects `execute` for statements that return rows.
let database_ok = match state.database.connect().await {
Ok(conn) => conn.query("SELECT 1", ()).await.is_ok(),
Err(_) => false,
};
let (status, database) = if database_ok {
(StatusCode::OK, "ok")
} else {
(StatusCode::SERVICE_UNAVAILABLE, "unavailable")
};
let body = Health {
status: if database_ok { "ok" } else { "degraded" },
app: state.config.name.clone(),
database,
};
(status, Json(body))
}