rust-mcp-server 0.3.8

An MCP server for Rust development
mod command;
mod globals;
mod meta;
mod response;
mod rmcp_server;
mod serde_utils;
mod tool;
mod tools;
mod version;
mod workspace;

use anyhow::Context;
use clap::Parser;
use command::execute_command;
use response::Response;
use rmcp::ServiceExt;
use rmcp::service::QuitReason;
use tool::Tool;
use tracing_appender::rolling;
use tracing_subscriber::{EnvFilter, fmt};
use version::AppVersion;

const RMCP_VERSION: &str = env!("RMCP_VERSION");

#[derive(Parser, Debug)]
#[command(author, version = AppVersion, about = "Rust MCP Server", long_about = None)]
struct Args {
    /// Log level (error, warn, info, debug, trace)
    #[arg(long, default_value = "info")]
    log_level: String,

    /// Log file path (if not set, logs to stderr)
    #[arg(long)]
    log_file: Option<String>,

    /// Disable a tool by name. Can be specified multiple times.
    #[arg(long = "disable-tool")]
    disabled_tools: Vec<String>,

    /// Rust project workspace path. By default, uses the current directory.
    #[arg(long)]
    workspace: Option<String>,

    /// Default cargo registry to use for commands that support registry option
    #[arg(long)]
    registry: Option<String>,

    /// Generate tools.md documentation file and exit
    #[arg(long)]
    generate_docs: Option<String>,

    /// Disable experimental recommendations for agent in tool responses
    #[arg(long)]
    no_recommendations: bool,
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let args = Args::parse();

    // Set up logging
    let env_filter = EnvFilter::new(&args.log_level);
    if let Some(ref path) = args.log_file {
        // Use rolling log file (daily rotation, keep old logs)
        use std::path::Path;
        let log_path = Path::new(path);
        let (dir, file_name) = match (log_path.parent(), log_path.file_name()) {
            (Some(d), Some(f)) => (d, f),
            _ => (Path::new("."), log_path.as_os_str()),
        };
        let file_appender = rolling::daily(dir, file_name);
        fmt()
            .with_env_filter(env_filter)
            .with_writer(file_appender)
            .with_ansi(false)
            .init();
    }
    tracing::info!("Starting Rust MCP Server: {args:?}");
    tracing::info!("Server version: {}", AppVersion::version());
    tracing::info!("RMCP crate version: {RMCP_VERSION}");

    let detect_workspace = args.workspace.is_none();
    if let Some(workspace) = args.workspace {
        tracing::info!("Workspace root has been overridden: {workspace}");
        globals::set_workspace_root(workspace);
    } else {
        tracing::info!("No workspace root specified, workspace auto-detection enabled");
    }

    if let Some(registry) = args.registry {
        tracing::info!("Default cargo registry has been set: {registry}");
        globals::set_default_registry(registry);
    }

    let server = rmcp_server::Server::new(
        &args.disabled_tools,
        args.no_recommendations,
        detect_workspace,
    );

    // Handle documentation generation mode
    if let Some(output_file) = args.generate_docs {
        tracing::info!("Generating documentation to: {output_file}");
        let docs = server.generate_markdown_docs();
        std::fs::write(&output_file, docs).context("Failed to write documentation file")?;
        println!("Documentation generated successfully: {output_file}");
        return Ok(());
    }

    let service = server
        .serve(rmcp::transport::stdio())
        .await
        .context("Failed to start server")?;

    eprintln!("Rust MCP Server started on stdio");

    let result = service.waiting().await;

    match result {
        Ok(QuitReason::Closed) => tracing::info!("Server closed normally"),
        Ok(QuitReason::Cancelled) => tracing::info!("Server was cancelled"),
        Ok(QuitReason::JoinError(error)) => {
            tracing::error!("Server join error: {error}");
            return Err(error.into());
        }
        Ok(reason) => {
            tracing::info!("Server exited with reason: {reason:?}");
        }
        Err(error) => {
            tracing::error!("Server encountered an error: {error}");
            return Err(error.into());
        }
    }

    Ok(())
}