use anyhow::{anyhow, Result};
use clap::Parser;
use cortex_mem_config::Config;
use cortex_mem_core::llm::LLMClientImpl;
use cortex_mem_tools::MemoryOperations;
use rmcp::{transport::stdio, ServiceExt};
use std::path::PathBuf;
use std::sync::Arc;
use tracing::{error, info};
mod service;
use service::{AutoTriggerConfig, MemoryMcpService};
#[derive(Parser)]
#[command(name = "cortex-mem-mcp")]
#[command(about = "MCP server for Cortex Memory to enhance agent's memory layer")]
#[command(author = "Cortex-Mem Contributors")]
#[command(version)]
struct Cli {
#[arg(short, long, default_value = "config.toml")]
config: PathBuf,
#[arg(long, default_value = "default")]
tenant: String,
#[arg(long)]
user: Option<String>,
#[arg(long, default_value = "10")]
auto_trigger_threshold: usize,
#[arg(long, default_value = "300")]
auto_trigger_interval: u64,
#[arg(long, default_value = "120")]
auto_trigger_inactivity: u64,
#[arg(long, default_value = "false")]
no_auto_trigger: bool,
#[arg(long)]
log_file: Option<PathBuf>,
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
if let Some(ref log_file) = cli.log_file {
if let Some(parent) = log_file.parent() {
std::fs::create_dir_all(parent)?;
}
let file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(log_file)?;
tracing_subscriber::fmt()
.with_writer(std::sync::Arc::new(file))
.with_max_level(tracing::Level::INFO)
.init();
}
info!("Starting Cortex Memory MCP Server");
info!("Using configuration file: {:?}", cli.config);
info!("Tenant ID: {}", cli.tenant);
if let Some(ref uid) = cli.user {
info!("User ID: {}", uid);
}
let config = Config::load(&cli.config)?;
let data_dir = config.cortex.data_dir();
info!("Data directory: {}", data_dir);
let model_name = config.llm.model_efficient.clone();
let llm_config = cortex_mem_core::llm::LLMConfig {
api_base_url: config.llm.api_base_url,
api_key: config.llm.api_key,
model_efficient: config.llm.model_efficient,
temperature: config.llm.temperature,
max_tokens: config.llm.max_tokens as usize,
};
let llm_client = Arc::new(LLMClientImpl::new(llm_config)?);
info!("LLM client initialized with model: {}", model_name);
let operations = MemoryOperations::new(
&data_dir,
&cli.tenant,
llm_client,
&config.qdrant.url,
&config.qdrant.collection_name,
config.qdrant.api_key.as_deref(),
&config.embedding.api_base_url,
&config.embedding.api_key,
&config.embedding.model_name,
config.qdrant.embedding_dim,
cli.user, config.cortex.enable_intent_analysis,
).await?;
let operations = Arc::new(operations);
info!("MemoryOperations initialized successfully");
let auto_trigger_config = AutoTriggerConfig {
message_count_threshold: cli.auto_trigger_threshold,
min_process_interval_secs: cli.auto_trigger_interval,
inactivity_timeout_secs: cli.auto_trigger_inactivity,
enable_auto_trigger: !cli.no_auto_trigger,
};
info!(
"Auto-trigger config: threshold={}, interval={}s, inactivity={}s, enabled={}",
auto_trigger_config.message_count_threshold,
auto_trigger_config.min_process_interval_secs,
auto_trigger_config.inactivity_timeout_secs,
auto_trigger_config.enable_auto_trigger
);
let service = MemoryMcpService::with_config(operations, auto_trigger_config);
if auto_trigger_config.enable_auto_trigger {
service.start_inactivity_checker();
}
let running_service = service
.serve(stdio())
.await
.map_err(|e| anyhow!("Failed to start MCP server: {}", e))?;
info!("MCP server initialized successfully");
match running_service.waiting().await {
Ok(reason) => info!("Server shutdown: {:?}", reason),
Err(e) => error!("Server error: {:?}", e),
}
Ok(())
}