use clap::Parser;
use rmcp::ServiceExt;
use tracing_subscriber::EnvFilter;
use semantic_memory_mcp::bridge::{self, EmbedderBackend};
use semantic_memory_mcp::{http_server, server};
#[derive(Parser, Debug)]
#[command(name = "semantic-memory-mcp")]
#[command(about = "MCP server for semantic-memory — local-first knowledge management")]
struct Cli {
#[arg(long)]
memory_dir: String,
#[arg(long, default_value = "candle")]
embedder: EmbedderBackend,
#[arg(long)]
embedding_url: Option<String>,
#[arg(long)]
embedding_model: Option<String>,
#[arg(long)]
embedding_dims: Option<usize>,
#[arg(long)]
http_port: Option<u16>,
#[arg(long)]
http_only: bool,
#[arg(long)]
turbo_quant: bool,
#[arg(long)]
turbo_quant_bits: Option<u8>,
#[arg(long)]
turbo_quant_projections: Option<usize>,
#[arg(long, default_value = "lean")]
tool_profile: String,
}
fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_writer(std::io::stderr)
.init();
let cli = Cli::parse();
eprintln!("semantic-memory-mcp starting...");
eprintln!(" memory_dir: {}", cli.memory_dir);
eprintln!(" embedder: {:?}", cli.embedder);
match cli.embedder {
EmbedderBackend::Candle => {
eprintln!(
" model: {} ({}d) — in-process Candle, CPU-only",
cli.embedding_model.as_deref().unwrap_or("nomic-embed-text"),
cli.embedding_dims.unwrap_or(768)
);
}
EmbedderBackend::Ollama => {
let url = cli
.embedding_url
.as_deref()
.unwrap_or("http://localhost:11434");
eprintln!(
" embedding: {} @ {} ({}d) — Ollama GPU-accelerated",
cli.embedding_model.as_deref().unwrap_or("nomic-embed-text"),
url,
cli.embedding_dims.unwrap_or(768)
);
let model = cli.embedding_model.as_deref().unwrap_or("nomic-embed-text");
if let Err(e) = reqwest::blocking::get(format!("{}/api/tags", url)) {
eprintln!(" WARNING: Ollama not reachable at {} — {}", url, e);
eprintln!(" Make sure Ollama is running: `ollama serve`");
eprintln!(" And the model is pulled: `ollama pull {}`", model);
eprintln!(" Falling back would require restart with --embedder candle");
anyhow::bail!("Ollama not reachable at {}", url);
} else {
eprintln!(" Ollama health check: OK");
}
}
EmbedderBackend::Mock => {
eprintln!(
" mock embedder ({}d) — for testing only",
cli.embedding_dims.unwrap_or(768)
);
}
}
let bridge_config = bridge::BridgeConfig::from_args(
&cli.memory_dir,
cli.embedder,
cli.embedding_url.as_deref(),
cli.embedding_model.as_deref(),
cli.embedding_dims,
cli.turbo_quant,
cli.turbo_quant_bits,
cli.turbo_quant_projections,
);
let bridge = bridge::MemoryBridge::open(bridge_config)?;
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.worker_threads(4)
.build()?;
let server = server::SemanticMemoryServer::new(bridge.clone(), &cli.tool_profile);
if let Some(port) = cli.http_port {
http_server::start_http_server(port, bridge, rt.handle().clone());
}
if cli.http_only {
eprintln!("HTTP-only mode: stdio MCP disabled, serving HTTP requests.");
rt.block_on(async {
std::future::pending::<()>().await;
});
return Ok(());
}
rt.block_on(async {
let service = server.serve(rmcp::transport::stdio()).await?;
service.waiting().await?;
Ok::<(), anyhow::Error>(())
})?;
Ok(())
}