//! Stdio entry point for lumotia-mcp. Reads newline-delimited JSON-RPC messages //! from stdin, dispatches via `lumotia_mcp::handle_message`, writes responses to //! stdout. Logs land on stderr so they don't collide with the JSON-RPC stream. use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; #[tokio::main(flavor = "current_thread")] async fn main() -> anyhow::Result<()> { let db_path = lumotia_storage::database_path(); eprintln!( "[lumotia-mcp] opening Lumotia database at {} (read-only)", db_path.display() ); // Open read-only at the connection level so the MCP server cannot write // to the user's database, regardless of which tools the dispatcher // exposes. Migrations are deliberately skipped โ€” this binary never owns // the schema; the main app is the single migration writer. let pool = lumotia_storage::init_readonly(&db_path).await?; eprintln!("[lumotia-mcp] ready, waiting for JSON-RPC on stdin"); let mut lines = BufReader::new(tokio::io::stdin()).lines(); let mut stdout = tokio::io::stdout(); while let Some(line) = lines.next_line().await? { let trimmed = line.trim(); if trimmed.is_empty() { continue; } let response = match serde_json::from_str::(trimmed) { Ok(raw) => match lumotia_mcp::handle_message(&pool, raw).await { Some(response) => response, None => continue, // notification โ€” no reply }, Err(err) => { // Per JSON-RPC 2.0 ยง5.1: a Parse Error responds with // code -32700 and id null. Previously this branch // logged and continued, dropping the response โ€” // clients saw silence instead of a structured error // (2026-04-22 review MAJOR). eprintln!("[lumotia-mcp] parse error: {err}"); lumotia_mcp::parse_error_response(&err.to_string()) } }; let payload = serde_json::to_string(&response)?; stdout.write_all(payload.as_bytes()).await?; stdout.write_all(b"\n").await?; stdout.flush().await?; } Ok(()) }