mcp_sync/commands/
init.rs

1//! `mcp-sync init` command - interactive mcp.yaml creation.
2
3use anyhow::Result;
4use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select};
5use std::fs;
6use std::path::Path;
7
8/// Server kind options.
9const SERVER_KINDS: &[&str] = &["stdio (local process)", "http (remote server)"];
10
11/// Runs the interactive init wizard.
12pub fn run(output_path: &Path) -> Result<()> {
13    let theme = ColorfulTheme::default();
14    
15    println!("šŸš€ MCP-Sync Configuration Wizard\n");
16    
17    if output_path.exists() {
18        let overwrite = Confirm::with_theme(&theme)
19            .with_prompt(format!("{} already exists. Overwrite?", output_path.display()))
20            .default(false)
21            .interact()?;
22        
23        if !overwrite {
24            println!("Aborted.");
25            return Ok(());
26        }
27    }
28    
29    let mut servers = Vec::new();
30    
31    loop {
32        println!("\n--- Add Server ---");
33        
34        let name: String = Input::with_theme(&theme)
35            .with_prompt("Server name")
36            .interact_text()?;
37        
38        let kind_idx = Select::with_theme(&theme)
39            .with_prompt("Server kind")
40            .items(SERVER_KINDS)
41            .default(0)
42            .interact()?;
43        
44        let server_config = if kind_idx == 0 {
45            // stdio
46            let command: String = Input::with_theme(&theme)
47                .with_prompt("Command")
48                .default("npx".to_string())
49                .interact_text()?;
50            
51            let args_str: String = Input::with_theme(&theme)
52                .with_prompt("Arguments (space-separated)")
53                .default("-y @modelcontextprotocol/server".to_string())
54                .interact_text()?;
55            
56            let args: Vec<String> = args_str
57                .split_whitespace()
58                .map(|s| s.to_string())
59                .collect();
60            
61            format!(
62                "  {}:\n    command: {}\n    args: [{}]",
63                name,
64                command,
65                args.iter()
66                    .map(|a| format!("\"{}\"", a))
67                    .collect::<Vec<_>>()
68                    .join(", ")
69            )
70        } else {
71            // http
72            let url: String = Input::with_theme(&theme)
73                .with_prompt("URL")
74                .default("https://mcp.example.com/v1".to_string())
75                .interact_text()?;
76            
77            format!("  {}:\n    kind: http\n    url: {}", name, url)
78        };
79        
80        servers.push(server_config);
81        
82        let add_more = Confirm::with_theme(&theme)
83            .with_prompt("Add another server?")
84            .default(false)
85            .interact()?;
86        
87        if !add_more {
88            break;
89        }
90    }
91    
92    // Ask about env-expander plugin
93    let use_env_expander = Confirm::with_theme(&theme)
94        .with_prompt("Enable env-expander plugin (expand ${VAR} in values)?")
95        .default(true)
96        .interact()?;
97    
98    // Generate YAML
99    let mut yaml = String::from("version: 1\n\n");
100    
101    if use_env_expander {
102        yaml.push_str("plugins:\n  - name: env-expander\n\n");
103    }
104    
105    yaml.push_str("servers:\n");
106    for server in servers {
107        yaml.push_str(&server);
108        yaml.push('\n');
109    }
110    
111    // Write file
112    if let Some(parent) = output_path.parent() {
113        fs::create_dir_all(parent)?;
114    }
115    fs::write(output_path, &yaml)?;
116    
117    println!("\nāœ… Created {}", output_path.display());
118    println!("\nNext steps:");
119    println!("  mcp-sync validate --canon {}", output_path.display());
120    println!("  mcp-sync sync --canon {}", output_path.display());
121    
122    Ok(())
123}