#![allow(clippy::uninlined_format_args)]
use clap::{Parser, Subcommand};
use codex_memory::{Config, MCPServer, Storage};
use std::fs;
use std::process;
use std::sync::Arc;
use tokio::signal;
use tracing::{error, info, warn};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
#[derive(Parser)]
#[command(name = "codex-memory")]
#[command(about = "Simple text storage service with MCP interface")]
#[command(version = env!("CARGO_PKG_VERSION"))]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Mcp,
Setup {
#[arg(long)]
skip_db_creation: bool,
},
Store {
content: String,
#[arg(long)]
context: String,
#[arg(long)]
summary: String,
#[arg(long)]
tags: Option<String>,
},
Get {
id: String,
},
Stats,
}
const MCP_PID_FILE: &str = "/tmp/codex-memory-mcp.pid";
fn ensure_singleton() -> Result<(), Box<dyn std::error::Error>> {
let current_pid = process::id();
if let Ok(pid_content) = fs::read_to_string(MCP_PID_FILE) {
if let Ok(existing_pid) = pid_content.trim().parse::<u32>() {
#[cfg(unix)]
{
use std::process::Command;
let output = Command::new("kill")
.args(["-0", &existing_pid.to_string()])
.output();
match output {
Ok(result) if result.status.success() => {
eprintln!(
"MCP server already running with PID {}, allowing additional instance",
existing_pid
);
let alt_pid_file = format!("/tmp/codex-memory-mcp-{}.pid", current_pid);
fs::write(&alt_pid_file, current_pid.to_string())?;
info!(
"MCP server starting with PID {} (alternative instance)",
current_pid
);
return Ok(());
}
_ => {
eprintln!("Cleaning up stale PID file for process {}", existing_pid);
let _ = fs::remove_file(MCP_PID_FILE);
}
}
}
}
}
fs::write(MCP_PID_FILE, current_pid.to_string())?;
info!("MCP server starting with PID {}", current_pid);
Ok(())
}
fn cleanup_pid_file() {
if let Err(e) = fs::remove_file(MCP_PID_FILE) {
error!("Failed to remove PID file: {}", e);
} else {
info!("Cleaned up PID file");
}
}
async fn setup_signal_handlers() -> Result<(), Box<dyn std::error::Error>> {
use signal::unix::{SignalKind, signal};
let mut sigterm = signal(SignalKind::terminate())?;
let mut sigint = signal(SignalKind::interrupt())?;
let mut sigquit = signal(SignalKind::quit())?;
let mut sighup = signal(SignalKind::hangup())?;
tokio::select! {
_ = sigterm.recv() => {
info!("Received SIGTERM, initiating graceful shutdown...");
}
_ = sigint.recv() => {
info!("Received SIGINT, initiating graceful shutdown...");
}
_ = sigquit.recv() => {
info!("Received SIGQUIT, initiating graceful shutdown...");
}
_ = sighup.recv() => {
info!("Received SIGHUP, initiating graceful shutdown...");
}
_ = signal::ctrl_c() => {
info!("Received Ctrl+C, initiating graceful shutdown...");
}
}
info!("Starting graceful shutdown sequence...");
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
cleanup_pid_file();
info!("Graceful shutdown complete");
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
let config = Config::from_env()?;
if matches!(cli.command, Commands::Mcp) {
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::from_default_env())
.with(
tracing_subscriber::fmt::layer()
.with_writer(std::io::stderr)
.with_target(false),
)
.init();
} else {
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::from_default_env())
.with(tracing_subscriber::fmt::layer())
.init();
}
match cli.command {
Commands::Setup { skip_db_creation } => {
if !skip_db_creation {
codex_memory::database::setup_local_database().await?;
}
println!("\n📝 Running database migrations...");
let pool = codex_memory::database::create_pool(&config.database_url).await?;
codex_memory::database::run_migrations(&pool).await?;
println!("✅ Migrations complete!");
println!("\n🎉 Setup complete! You can now run:");
println!(" codex-memory mcp # Start MCP server");
println!(" codex-memory store \"Your text here\" --context \"example\" --summary \"test\" # Store content");
println!(" codex-memory stats # View statistics");
}
Commands::Mcp => {
ensure_singleton()?;
let shutdown_handler = tokio::spawn(async {
if let Err(e) = setup_signal_handlers().await {
error!("Signal handler setup failed: {}", e);
}
});
info!("Initializing database connection and running migrations...");
let pool = codex_memory::database::create_pool(&config.database_url).await?;
codex_memory::database::run_migrations(&pool).await?;
let storage = Arc::new(Storage::new(pool));
info!("Starting MCP server in stdio mode...");
let server = MCPServer::new(config, storage);
tokio::select! {
result = server.run_stdio() => {
info!("MCP server finished normally");
cleanup_pid_file();
result?;
}
_ = shutdown_handler => {
info!("Graceful shutdown initiated by signal");
cleanup_pid_file();
}
_ = tokio::time::sleep(tokio::time::Duration::from_secs(30)) => {
warn!("Shutdown timeout exceeded, forcing exit");
cleanup_pid_file();
std::process::exit(1);
}
}
}
Commands::Store {
content,
context,
summary,
tags,
} => {
let pool = codex_memory::database::create_pool(&config.database_url).await?;
codex_memory::database::run_migrations(&pool).await?;
let storage = Arc::new(Storage::new(pool));
let tag_list = tags.map(|t| {
t.split(',')
.map(|s| s.trim().to_string())
.collect::<Vec<_>>()
});
let id = storage
.store(&content, context.clone(), summary.clone(), tag_list)
.await?;
println!("Stored with ID: {}", id);
println!("Context: {}", context);
println!("Summary: {}", summary);
}
Commands::Get { id } => {
let pool = codex_memory::database::create_pool(&config.database_url).await?;
codex_memory::database::run_migrations(&pool).await?;
let storage = Arc::new(Storage::new(pool));
let uuid = id.parse::<uuid::Uuid>()?;
match storage.get(uuid).await? {
Some(memory) => {
println!("Content: {}", memory.content);
println!("Context: {}", memory.context);
println!("Summary: {}", memory.summary);
println!("Created: {}", memory.created_at);
if !memory.tags.is_empty() {
println!("Tags: {:?}", memory.tags);
}
}
None => {
println!("Content not found");
}
}
}
Commands::Stats => {
let pool = codex_memory::database::create_pool(&config.database_url).await?;
codex_memory::database::run_migrations(&pool).await?;
let storage = Arc::new(Storage::new(pool));
let stats = storage.stats().await?;
println!("Total memories: {}", stats.total_memories);
println!("Table size: {}", stats.table_size);
if let Some(last) = stats.last_memory_created {
println!("Last created: {}", last);
}
}
}
Ok(())
}