Skip to main content

turbomcp_cli/
dev.rs

1//! Development server command with hot reload support.
2//!
3//! This module implements the `turbomcp dev` command which provides:
4//! - Hot reload development server using cargo-watch
5//! - MCP Inspector integration for debugging
6//!
7//! # Usage
8//!
9//! ```bash
10//! # Run server with hot reload
11//! turbomcp dev ./my-server --watch
12//!
13//! # Run with inspector
14//! turbomcp dev ./my-server --inspector
15//!
16//! # Build in release mode
17//! turbomcp dev ./my-server --release
18//! ```
19
20use std::path::Path;
21use std::process::{Command, Stdio};
22
23use anyhow::{Context, Result, bail};
24
25use crate::cli::DevArgs;
26
27/// Execute the dev command.
28pub fn execute(args: &DevArgs) -> Result<()> {
29    let path = &args.path;
30
31    // Determine if this is a cargo project or a binary
32    let is_cargo_project = path.is_dir() && path.join("Cargo.toml").exists();
33    let is_binary = path.is_file() && is_executable(path);
34
35    if !is_cargo_project && !is_binary {
36        bail!(
37            "Path '{}' is neither a Cargo project nor an executable binary.\n\
38             For a Cargo project, provide the directory containing Cargo.toml.\n\
39             For a binary, provide the path to the executable.",
40            path.display()
41        );
42    }
43
44    if args.inspector {
45        eprintln!(
46            "Warning: --inspector / --inspector-port are not yet implemented and will be ignored."
47        );
48    }
49
50    if args.watch {
51        run_with_watch(args, is_cargo_project)
52    } else if is_cargo_project {
53        run_cargo_project(args)
54    } else {
55        run_binary(args)
56    }
57}
58
59/// Run with cargo-watch for hot reload.
60fn run_with_watch(args: &DevArgs, is_cargo_project: bool) -> Result<()> {
61    // Check if cargo-watch is installed
62    if !is_command_available("cargo-watch") {
63        eprintln!("cargo-watch is not installed. Install it with:");
64        eprintln!("  cargo install cargo-watch");
65        eprintln!();
66        eprintln!("Then run this command again.");
67        bail!("cargo-watch not found");
68    }
69
70    if !is_cargo_project {
71        bail!("--watch requires a Cargo project directory, not a binary");
72    }
73
74    println!("Starting development server with hot reload...");
75    println!("  Project: {}", args.path.display());
76    println!("  Mode: {}", if args.release { "release" } else { "debug" });
77    println!();
78    println!("Press Ctrl+C to stop.");
79    println!();
80
81    let mut cmd = Command::new("cargo");
82    cmd.arg("watch")
83        .arg("-x")
84        .arg(build_cargo_run_args(args))
85        .current_dir(&args.path)
86        .stdin(Stdio::inherit())
87        .stdout(Stdio::inherit())
88        .stderr(Stdio::inherit());
89
90    let status = cmd.status().context("Failed to run cargo-watch")?;
91
92    if !status.success() {
93        bail!("cargo-watch exited with non-zero status");
94    }
95
96    Ok(())
97}
98
99/// Run a Cargo project directly (no watch).
100fn run_cargo_project(args: &DevArgs) -> Result<()> {
101    println!("Starting development server...");
102    println!("  Project: {}", args.path.display());
103    println!("  Mode: {}", if args.release { "release" } else { "debug" });
104    println!();
105
106    let mut cmd = Command::new("cargo");
107    cmd.arg("run");
108
109    if args.release {
110        cmd.arg("--release");
111    }
112
113    if !args.server_args.is_empty() {
114        cmd.arg("--");
115        cmd.args(&args.server_args);
116    }
117
118    cmd.current_dir(&args.path)
119        .stdin(Stdio::inherit())
120        .stdout(Stdio::inherit())
121        .stderr(Stdio::inherit());
122
123    let status = cmd.status().context("Failed to run cargo")?;
124
125    if !status.success() {
126        bail!("Server exited with non-zero status");
127    }
128
129    Ok(())
130}
131
132/// Run a binary directly.
133fn run_binary(args: &DevArgs) -> Result<()> {
134    println!("Starting MCP server...");
135    println!("  Binary: {}", args.path.display());
136    println!();
137
138    let mut cmd = Command::new(&args.path);
139    cmd.args(&args.server_args)
140        .stdin(Stdio::inherit())
141        .stdout(Stdio::inherit())
142        .stderr(Stdio::inherit());
143
144    let status = cmd.status().context("Failed to run server binary")?;
145
146    if !status.success() {
147        bail!("Server exited with non-zero status");
148    }
149
150    Ok(())
151}
152
153/// Build the cargo run arguments string for cargo-watch.
154fn build_cargo_run_args(args: &DevArgs) -> String {
155    let mut cmd_args = vec!["run".to_string()];
156
157    if args.release {
158        cmd_args.push("--release".to_string());
159    }
160
161    if !args.server_args.is_empty() {
162        cmd_args.push("--".to_string());
163        cmd_args.extend(args.server_args.clone());
164    }
165
166    cmd_args.join(" ")
167}
168
169/// Check if a command is available in PATH using the cross-platform `which` crate
170/// (works on Windows, macOS, and Linux without shelling out).
171fn is_command_available(cmd: &str) -> bool {
172    which::which(cmd).is_ok()
173}
174
175/// Check if a path is an executable file.
176fn is_executable(path: &Path) -> bool {
177    #[cfg(unix)]
178    {
179        use std::os::unix::fs::PermissionsExt;
180        path.metadata()
181            .map(|m| m.is_file() && (m.permissions().mode() & 0o111) != 0)
182            .unwrap_or(false)
183    }
184
185    #[cfg(not(unix))]
186    {
187        path.is_file()
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194    use std::path::PathBuf;
195
196    #[test]
197    fn test_build_cargo_run_args_basic() {
198        let args = DevArgs {
199            path: PathBuf::from("."),
200            watch: false,
201            server_args: vec![],
202            release: false,
203            inspector: false,
204            inspector_port: 5173,
205        };
206
207        assert_eq!(build_cargo_run_args(&args), "run");
208    }
209
210    #[test]
211    fn test_build_cargo_run_args_release() {
212        let args = DevArgs {
213            path: PathBuf::from("."),
214            watch: false,
215            server_args: vec![],
216            release: true,
217            inspector: false,
218            inspector_port: 5173,
219        };
220
221        assert_eq!(build_cargo_run_args(&args), "run --release");
222    }
223
224    #[test]
225    fn test_build_cargo_run_args_with_server_args() {
226        let args = DevArgs {
227            path: PathBuf::from("."),
228            watch: false,
229            server_args: vec!["--port".to_string(), "8080".to_string()],
230            release: false,
231            inspector: false,
232            inspector_port: 5173,
233        };
234
235        assert_eq!(build_cargo_run_args(&args), "run -- --port 8080");
236    }
237}