fs-mcp-rs 1.2.4

A fast, configurable filesystem MCP server with explicit root isolation and bounded I/O
Documentation
//! Interactive CLI setup wizard for creating fs-mcp-rs configuration files.

use anyhow::{Context, Result};
use inquire::{Confirm, Select, Text};
use std::fs;
use std::path::{Path, PathBuf};

use crate::cli_format::print_client_snippets;

/// Runs an interactive terminal wizard using arrow keys and prompts to configure `fs-mcp-rs`.
pub fn run_wizard(output_path: &Path) -> Result<PathBuf> {
    println!("\n================================================================================");
    println!("             Welcome to fs-mcp-rs Interactive Setup Wizard                       ");
    println!("================================================================================");
    println!("This wizard will generate a customized `config.toml` for your MCP server.\n");

    // 1. Filesystem Roots
    let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
    let default_root = current_dir.to_string_lossy().to_string();

    let root_path_str = Text::new("Root Directory to expose over MCP:")
        .with_default(&default_root)
        .with_help_message("Allowed directory path. Relative or absolute.")
        .prompt()
        .context("Wizard cancelled")?;

    let root_path = PathBuf::from(root_path_str.trim());

    // 2. Read-Only Mode
    let read_only_options = vec![
        "Read-Write Mode (Allow creating, editing, and removing files)",
        "Read-Only Mode (Safe mode - disable all file mutations)",
    ];

    let selected_mode = Select::new("Select Filesystem Access Mode:", read_only_options)
        .prompt()
        .context("Wizard cancelled")?;

    let read_only = selected_mode.starts_with("Read-Only");

    // 3. Terminal Execution Mode
    let terminal_options = vec![
        "Disabled (Safe - terminal execution tools unavailable)",
        "Enabled (Warning: allows AI clients to run shell commands)",
    ];

    let selected_terminal =
        Select::new("Select Terminal Command Execution Mode:", terminal_options)
            .prompt()
            .context("Wizard cancelled")?;

    let terminal_enabled = selected_terminal.starts_with("Enabled");

    // 4. Tool Call Logging Mode
    let log_options = vec![
        "Enabled (Concise [OK]/[WARN] log lines for every tool call)",
        "Disabled (Quiet mode - no tool call logs)",
    ];

    let selected_log = Select::new("Select Tool Call Logging Mode:", log_options)
        .prompt()
        .context("Wizard cancelled")?;

    let log_tools = selected_log.starts_with("Enabled");

    // 5. Server Listener Host and Port
    let host_str = Text::new("Server Host IP:")
        .with_default("127.0.0.1")
        .prompt()
        .context("Wizard cancelled")?;

    let port_str = Text::new("Server Port:")
        .with_default("8000")
        .prompt()
        .context("Wizard cancelled")?;

    let port: u16 = port_str.trim().parse().unwrap_or(8000);

    // 6. Config file destination
    let output_str = Text::new("Save Configuration File to:")
        .with_default(&output_path.to_string_lossy())
        .prompt()
        .context("Wizard cancelled")?;

    let final_output_path = PathBuf::from(output_str.trim());

    // Generate TOML content
    let root_formatted = root_path.to_string_lossy().replace('\\', "/");
    let config_content = format!(
        r#"# Generated by fs-mcp-rs interactive wizard
# Config file for fs-mcp-rs Model Context Protocol server

[server]
host = "{host}"
port = {port}
max_concurrency = 32
max_io_concurrency = 16
log_tools = {log_tools}

[filesystem]
roots = ["{root}"]
read_only = {read_only}
max_read_bytes = 8388608
max_write_bytes = 8388608
follow_links = false
tree_max_depth = 8
tree_max_entries = 1000
tree_max_warnings = 32
patch_max_bytes = 1048576
patch_preview_bytes = 16384

[search]
max_results = 1000
max_concurrency = 4
worker_threads = 4
regex_cache_capacity = 64
include_hidden = false
respect_gitignore = true

[terminal]
enabled = {terminal_enabled}
max_concurrency = 2
default_timeout_ms = 30000
max_timeout_ms = 300000
max_output_bytes = 4194304
max_read_bytes = 262144
max_wait_ms = 30000
session_retention_ms = 300000

[oauth]
enabled = true
require_auth = false
"#,
        host = host_str.trim(),
        port = port,
        log_tools = log_tools,
        root = root_formatted,
        read_only = read_only,
        terminal_enabled = terminal_enabled
    );

    if let Some(parent) = final_output_path.parent() {
        if !parent.as_os_str().is_empty() && !parent.exists() {
            fs::create_dir_all(parent)
                .with_context(|| format!("Failed to create directory {}", parent.display()))?;
        }
    }

    fs::write(&final_output_path, config_content).with_context(|| {
        format!(
            "Failed to write configuration to {}",
            final_output_path.display()
        )
    })?;

    println!(
        "\n[OK] Configuration successfully saved to: {}",
        final_output_path.display()
    );

    let show_snippets =
        Confirm::new("Would you like to view AI client configuration snippets now?")
            .with_default(true)
            .prompt()
            .unwrap_or(false);

    if show_snippets {
        print_client_snippets(&final_output_path, "fs-mcp-rs");
    }

    Ok(final_output_path)
}