AkurAI Build
Menu

Public repository / a8fcb834ff1ee338e6ee652da5580028f8c2b6d6

AkurAI-Notes / crates/notes-cli/src/cmd_serve.rs

Latest change bcb45cd596a14954231c7caa6751f70d6b09a1df - chore: drop RequestLogger middleware (framework access log is built-in, default on) by Ólafur Búi Ólafsson

use akurai_http::{Method, MiddlewareStack, Request, Response, SecurityHeaders, Server};
use notes_api::auth::idp_client::IdpConfig;
use notes_api::{build_router, NotesState, Route};
use notes_mcp::server::{McpConfig, McpServer};
use std::io;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};

pub struct Config {
    pub host: String,
    pub port: u16,
    pub dir: PathBuf,
    pub db: String,
}

pub fn run(cfg: Config) -> io::Result<()> {
    let idp_cfg = IdpConfig::from_env();
    if idp_cfg.client_id.is_empty() {
        return Err(io::Error::other("AKURAI_NOTES_IDP_CLIENT_ID is required"));
    }

    let state = NotesState::new(&cfg.db, cfg.dir.clone(), idp_cfg).map_err(io::Error::other)?;
    let mcp_token = std::env::var("AKURAI_NOTES_MCP_TOKEN").unwrap_or_default();
    let mcp_owner = std::env::var("AKURAI_NOTES_MCP_OWNER_SUB").unwrap_or_default();
    if mcp_token.is_empty() || mcp_owner.is_empty() {
        return Err(io::Error::other(
            "AKURAI_NOTES_MCP_TOKEN and AKURAI_NOTES_MCP_OWNER_SUB are required",
        ));
    }
    let mcp = Arc::new(McpServer::with_db(
        McpConfig {
            host: cfg.host.clone(),
            port: cfg.port,
            db: cfg.db.clone(),
            token: mcp_token,
            embed_url: state.embed_url.clone(),
            embed_model: state.embed_model.clone(),
            embed_api_key: state.embed_api_key.clone(),
            owner_sub: mcp_owner,
        },
        Arc::clone(&state.db),
    ));
    let state = Arc::new(Mutex::new(state));

    let routes = build_router(state);

    let addr = format!("{}:{}", cfg.host, cfg.port);
    let server = Server::bind(&addr)?;
    let local = server.local_addr()?;

    println!("AkurAI-Notes");
    println!("  database: {}", cfg.db);
    println!("  frontend: {}", cfg.dir.display());
    println!("  → http://{local}");

    let stack = MiddlewareStack::new().push(SecurityHeaders);

    server.run_with(stack, move |req: &Request| -> Response {
        if req.path == "/mcp" {
            mcp.handle_http(req)
        } else {
            dispatch(&routes, req)
        }
    })
}

fn dispatch(routes: &[Route], req: &Request) -> Response {
    let mut best: Option<(Response, (usize, usize, usize))> = None;

    for route in routes {
        if !method_matches(route.method, &req.method) {
            continue;
        }
        let pattern = akurai_router::Pattern::parse(route.pattern);
        if pattern.match_path(&req.path).is_some() {
            let score = pattern.specificity();
            if best.as_ref().is_none_or(|(_, b)| score > *b) {
                let resp = (route.handler)(req);
                best = Some((resp, score));
            }
        }
    }

    match best {
        Some((resp, _)) => resp,
        None => Response::not_found(),
    }
}

fn method_matches(pattern: &str, method: &Method) -> bool {
    match method {
        Method::Get => pattern.eq_ignore_ascii_case("GET"),
        Method::Post => pattern.eq_ignore_ascii_case("POST"),
        Method::Put => pattern.eq_ignore_ascii_case("PUT"),
        Method::Patch => pattern.eq_ignore_ascii_case("PATCH"),
        Method::Delete => pattern.eq_ignore_ascii_case("DELETE"),
        Method::Head => pattern.eq_ignore_ascii_case("HEAD"),
        Method::Options => pattern.eq_ignore_ascii_case("OPTIONS"),
        Method::Other(s) => pattern.eq_ignore_ascii_case(s),
    }
}