use anyhow::Result;
use clap::{Parser, Subcommand};
use rmcp::{ServiceExt, transport::stdio};
use std::path::PathBuf;
use std::process;
use tracing_subscriber::EnvFilter;
mod doctor;
mod update;
use rust_docs_mcp::RustDocsService;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
#[arg(long, env = "RUST_DOCS_MCP_CACHE_DIR")]
cache_dir: Option<PathBuf>,
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand, Debug)]
enum Commands {
Install {
#[arg(long)]
target_dir: Option<PathBuf>,
#[arg(long)]
force: bool,
},
Update {
#[arg(long)]
target_dir: Option<PathBuf>,
#[arg(long)]
repo_url: Option<String>,
#[arg(long)]
branch: Option<String>,
},
Doctor {
#[arg(long)]
json: bool,
},
}
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse();
if let Some(command) = args.command {
return handle_command(command, args.cache_dir).await;
}
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env().add_directive(tracing::Level::INFO.into()))
.with_writer(std::io::stderr)
.with_ansi(false)
.init();
tracing::info!("Starting MCP Rust Docs server on stdio...");
if let Some(ref cache_dir) = args.cache_dir {
tracing::info!("Using custom cache directory: {}", cache_dir.display());
}
let rust_docs_service = RustDocsService::new(args.cache_dir)?;
let service = rust_docs_service.serve(stdio()).await.inspect_err(|e| {
tracing::error!("serving error: {:?}", e);
})?;
service.waiting().await?;
Ok(())
}
async fn handle_command(command: Commands, cache_dir: Option<PathBuf>) -> Result<()> {
match command {
Commands::Install { target_dir, force } => install_executable(target_dir, force).await,
Commands::Update {
target_dir,
repo_url,
branch,
} => update::update_executable(target_dir, repo_url, branch).await,
Commands::Doctor { json } => handle_doctor_command(cache_dir, json).await,
}
}
async fn install_executable(target_dir: Option<PathBuf>, force: bool) -> Result<()> {
use std::env;
use std::fs;
let current_exe = env::current_exe()?;
let target_dir = match target_dir {
Some(dir) => dir,
None => {
let home = dirs::home_dir()
.ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?;
home.join(".local").join("bin")
}
};
fs::create_dir_all(&target_dir)?;
let target_file = target_dir.join("rust-docs-mcp");
if target_file.exists() && !force {
eprintln!(
"Error: {} already exists. Use --force to overwrite.",
target_file.display()
);
process::exit(1);
}
fs::copy(¤t_exe, &target_file)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&target_file)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(&target_file, perms)?;
}
println!(
"Successfully installed rust-docs-mcp to {}",
target_file.display()
);
if let Ok(path_var) = env::var("PATH") {
let path_separator = if cfg!(windows) { ';' } else { ':' };
let paths: Vec<&str> = path_var.split(path_separator).collect();
let target_dir_str = target_dir.to_string_lossy();
if !paths.iter().any(|&p| p == target_dir_str) {
println!("\nWarning: {} is not in your PATH.", target_dir.display());
println!(
"Add the following line to your shell configuration file (.bashrc, .zshrc, etc.):"
);
println!("export PATH=\"{}:$PATH\"", target_dir.display());
} else {
println!("\nYou can now run 'rust-docs-mcp' from anywhere in your terminal.");
}
}
doctor::run_and_print_diagnostics().await?;
Ok(())
}
async fn handle_doctor_command(cache_dir: Option<PathBuf>, json_output: bool) -> Result<()> {
let results = doctor::run_diagnostics(cache_dir).await?;
if json_output {
doctor::print_results_json(&results)?;
} else {
doctor::print_results(&results);
}
process::exit(doctor::exit_code(&results));
}