use anyhow::Result;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct McpProbePaths {
pub home_dir: PathBuf,
pub logs_dir: PathBuf,
pub reports_dir: PathBuf,
pub sessions_dir: PathBuf,
pub config_dir: PathBuf,
}
impl McpProbePaths {
pub fn new() -> Result<Self> {
let home_dir = Self::get_mcp_probe_home()?;
let paths = Self {
logs_dir: home_dir.join("logs"),
reports_dir: home_dir.join("reports"),
sessions_dir: home_dir.join("sessions"),
config_dir: home_dir.join("config"),
home_dir,
};
paths.ensure_directories_exist()?;
Ok(paths)
}
fn get_mcp_probe_home() -> Result<PathBuf> {
let home = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.unwrap_or_else(|_| ".".to_string());
Ok(Path::new(&home).join(".mcp-probe"))
}
fn ensure_directories_exist(&self) -> Result<()> {
for dir in [
&self.home_dir,
&self.logs_dir,
&self.reports_dir,
&self.sessions_dir,
&self.config_dir,
] {
std::fs::create_dir_all(dir)?;
}
Ok(())
}
pub fn log_file(&self, name: &str) -> PathBuf {
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
self.logs_dir.join(format!("{}-{}.log", name, timestamp))
}
pub fn debug_log_file(&self) -> PathBuf {
self.logs_dir.join("mcp-probe-debug.log")
}
pub fn report_file(&self, name: &str, extension: &str) -> PathBuf {
let date = chrono::Utc::now().format("%Y%m%d");
let timestamp = chrono::Utc::now().format("%H%M%S");
self.reports_dir
.join(format!("{}-{}-{}.{}", date, name, timestamp, extension))
}
#[allow(dead_code)]
pub fn dated_report_file(&self, name: &str, extension: &str) -> PathBuf {
let date = chrono::Utc::now().format("%Y%m%d");
self.reports_dir
.join(format!("{}-{}.{}", date, name, extension))
}
#[allow(dead_code)]
pub fn session_file(&self, name: &str) -> PathBuf {
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
self.sessions_dir
.join(format!("{}-{}.json", name, timestamp))
}
pub fn config_file(&self, name: &str) -> PathBuf {
self.config_dir.join(format!("{}.toml", name))
}
pub fn default_config_file(&self) -> PathBuf {
self.config_file("mcp-probe")
}
#[allow(dead_code)]
pub fn custom_output_dir(&self, name: &str) -> Result<PathBuf> {
let dir = self.reports_dir.join(name);
std::fs::create_dir_all(&dir)?;
Ok(dir)
}
#[allow(dead_code)]
pub fn temp_file(&self, name: &str, extension: &str) -> PathBuf {
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
self.home_dir
.join(format!("{}-{}.{}", name, timestamp, extension))
}
#[allow(dead_code)]
pub fn cleanup_old_files(&self, days_to_keep: u64) -> Result<()> {
let cutoff = chrono::Utc::now() - chrono::Duration::days(days_to_keep as i64);
for dir in [&self.logs_dir, &self.reports_dir, &self.sessions_dir] {
self.cleanup_directory(dir, cutoff)?;
}
Ok(())
}
#[allow(dead_code)]
fn cleanup_directory(&self, dir: &Path, cutoff: chrono::DateTime<chrono::Utc>) -> Result<()> {
if !dir.exists() {
return Ok(());
}
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let metadata = entry.metadata()?;
if metadata.is_file() {
if let Ok(modified) = metadata.modified() {
let modified_utc: chrono::DateTime<chrono::Utc> = modified.into();
if modified_utc < cutoff {
if let Err(e) = std::fs::remove_file(entry.path()) {
tracing::warn!("Failed to remove old file {:?}: {}", entry.path(), e);
} else {
tracing::debug!("Cleaned up old file: {:?}", entry.path());
}
}
}
}
}
Ok(())
}
#[allow(dead_code)]
pub fn relative_to_home(&self, path: &Path) -> Option<PathBuf> {
path.strip_prefix(&self.home_dir)
.ok()
.map(|p| p.to_path_buf())
}
pub fn print_structure(&self) {
println!("📁 MCP Probe Directory Structure:");
println!(" 🏠 Home: {}", self.home_dir.display());
println!(" 📄 Logs: {}", self.logs_dir.display());
println!(" 📊 Reports: {}", self.reports_dir.display());
println!(" 💾 Sessions: {}", self.sessions_dir.display());
println!(" ⚙️ Config: {}", self.config_dir.display());
}
}
impl Default for McpProbePaths {
fn default() -> Self {
Self::new().expect("Failed to create MCP Probe paths")
}
}
pub fn get_mcp_probe_paths() -> Result<McpProbePaths> {
McpProbePaths::new()
}
#[allow(dead_code)]
pub fn get_report_path(name: &str, extension: &str) -> Result<PathBuf> {
let paths = get_mcp_probe_paths()?;
Ok(paths.report_file(name, extension))
}
#[allow(dead_code)]
pub fn get_log_path(name: &str) -> Result<PathBuf> {
let paths = get_mcp_probe_paths()?;
Ok(paths.log_file(name))
}
#[allow(dead_code)]
pub fn get_session_path(name: &str) -> Result<PathBuf> {
let paths = get_mcp_probe_paths()?;
Ok(paths.session_file(name))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_path_generation() -> Result<()> {
let paths = McpProbePaths::new()?;
let log_path = paths.log_file("test");
assert!(log_path.to_string_lossy().contains("test-"));
assert!(log_path.extension().unwrap() == "log");
let report_path = paths.report_file("test-report", "json");
assert!(report_path.to_string_lossy().contains("test-report"));
assert!(report_path.extension().unwrap() == "json");
let dated_report = paths.dated_report_file("daily-report", "json");
assert!(dated_report.to_string_lossy().contains("daily-report"));
let session_path = paths.session_file("debug-session");
assert!(session_path.to_string_lossy().contains("debug-session"));
assert!(session_path.extension().unwrap() == "json");
Ok(())
}
#[test]
fn test_directory_creation() -> Result<()> {
let paths = McpProbePaths::new()?;
assert!(paths.home_dir.exists());
assert!(paths.logs_dir.exists());
assert!(paths.reports_dir.exists());
assert!(paths.sessions_dir.exists());
assert!(paths.config_dir.exists());
Ok(())
}
#[test]
fn test_helper_functions() -> Result<()> {
let report_path = get_report_path("test", "json")?;
assert!(report_path.extension().unwrap() == "json");
let log_path = get_log_path("test")?;
assert!(log_path.extension().unwrap() == "log");
let session_path = get_session_path("test")?;
assert!(session_path.extension().unwrap() == "json");
Ok(())
}
}