Menu
Public repository / fe83bc90d9cc0428d28b0dad315a6bc43a45efc6
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),
}
}