From 77a51cfd6f1b58fcb36919d967bf592b2d76fbd7 Mon Sep 17 00:00:00 2001 From: Stephen Waits <1+swaits@noreply.git.swaits.com> Date: Sun, 11 Oct 2026 03:01:39 +0000 Subject: [PATCH] Next steps 5-10: resources, cash flow, QB interop, backfill, HTTP, parallel-run plan --- crates/ledger-mcp/src/main.rs | 45 ++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/crates/ledger-mcp/src/main.rs b/crates/ledger-mcp/src/main.rs index 60e5055..ea7095b 100644 --- a/crates/ledger-mcp/src/main.rs +++ b/crates/ledger-mcp/src/main.rs @@ -36,14 +36,16 @@ fn default_data_dir() -> PathBuf { } fn print_usage() { - eprintln!("usage: ledger-mcp [--data-dir PATH] [--config PATH]"); + eprintln!("usage: ledger-mcp [--data-dir PATH] [--config PATH] [--http ADDR]"); eprintln!(" --data-dir directory holding journal.jsonl (default ~/.local/share/wanderbooks)"); eprintln!(" --config JSON config file (default /config.json, optional)"); + eprintln!(" --http serve over Streamable HTTP at ADDR (e.g. 127.0.0.1:8080) instead of stdio"); } fn main() -> Result<(), Box> { let mut data_dir = default_data_dir(); let mut config_path: Option = None; + let mut http_addr: Option = None; let mut args = std::env::args().skip(1); while let Some(arg) = args.next() { match arg.as_str() { @@ -53,6 +55,9 @@ fn main() -> Result<(), Box> { "--config" => { config_path = Some(PathBuf::from(args.next().ok_or("--config needs a path")?)); } + "--http" => { + http_addr = Some(args.next().ok_or("--http needs an address")?); + } "--help" | "-h" => { print_usage(); return Ok(()); @@ -88,15 +93,49 @@ fn main() -> Result<(), Box> { store .verify() .map_err(|e| format!("store verify failed: {e}"))?; - eprintln!("ledger-mcp: store verified, serving over stdio"); - let server = BooksServer::new(store, config.receipt_policy); let rt = tokio::runtime::Builder::new_multi_thread() .enable_all() .build()?; + if let Some(addr) = http_addr { + eprintln!("ledger-mcp: store verified, serving Streamable HTTP on {addr}"); + rt.block_on(serve_http(addr, store, config.receipt_policy))?; + return Ok(()); + } + + eprintln!("ledger-mcp: store verified, serving over stdio"); + let server = BooksServer::new(store, config.receipt_policy); rt.block_on(async { let running = server.serve(stdio()).await?; let _ = running.waiting().await; Ok::<(), Box>(()) }) } + +/// Serve MCP over Streamable HTTP (for network clients). +async fn serve_http( + addr: String, + store: Store, + policy: ReceiptPolicy, +) -> Result<(), Box> { + use rmcp::transport::streamable_http_server::{ + StreamableHttpService, session::local::LocalSessionManager, + }; + use std::sync::Arc; + + let store = Arc::new(std::sync::Mutex::new(store)); + let service = StreamableHttpService::new( + move || { + let server = server::BooksServer::new_shared(store.clone(), policy.clone()); + Ok(server) + }, + Arc::new(LocalSessionManager::default()), + Default::default(), + ); + + let router = axum::Router::new().nest_service("/mcp", service); + let listener = tokio::net::TcpListener::bind(&addr).await?; + eprintln!("ledger-mcp: listening on http://{addr}/mcp"); + axum::serve(listener, router).await?; + Ok(()) +}