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.watch {
45        run_with_watch(args, is_cargo_project)
46    } else if is_cargo_project {
47        run_cargo_project(args)
48    } else {
49        run_binary(args)
50    }
51}
52
53/// Run with cargo-watch for hot reload.
54fn run_with_watch(args: &DevArgs, is_cargo_project: bool) -> Result<()> {
55    // Check if cargo-watch is installed
56    if !is_command_available("cargo-watch") {
57        eprintln!("cargo-watch is not installed. Install it with:");
58        eprintln!("  cargo install cargo-watch");
59        eprintln!();
60        eprintln!("Then run this command again.");
61        bail!("cargo-watch not found");
62    }
63
64    if !is_cargo_project {
65        bail!("--watch requires a Cargo project directory, not a binary");
66    }
67
68    println!("Starting development server with hot reload...");
69    println!("  Project: {}", args.path.display());
70    println!("  Mode: {}", if args.release { "release" } else { "debug" });
71    println!();
72    println!("Press Ctrl+C to stop.");
73    println!();
74
75    let mut cmd = Command::new("cargo");
76    cmd.arg("watch")
77        .arg("-x")
78        .arg(build_cargo_run_args(args))
79        .current_dir(&args.path)
80        .stdin(Stdio::inherit())
81        .stdout(Stdio::inherit())
82        .stderr(Stdio::inherit());
83
84    let status = cmd.status().context("Failed to run cargo-watch")?;
85
86    if !status.success() {
87        bail!("cargo-watch exited with non-zero status");
88    }
89
90    Ok(())
91}
92
93/// Run a Cargo project directly (no watch).
94fn run_cargo_project(args: &DevArgs) -> Result<()> {
95    println!("Starting development server...");
96    println!("  Project: {}", args.path.display());
97    println!("  Mode: {}", if args.release { "release" } else { "debug" });
98    println!();
99
100    let mut cmd = Command::new("cargo");
101    cmd.arg("run");
102
103    if args.release {
104        cmd.arg("--release");
105    }
106
107    if !args.server_args.is_empty() {
108        cmd.arg("--");
109        cmd.args(&args.server_args);
110    }
111
112    cmd.current_dir(&args.path)
113        .stdin(Stdio::inherit())
114        .stdout(Stdio::inherit())
115        .stderr(Stdio::inherit());
116
117    let status = cmd.status().context("Failed to run cargo")?;
118
119    if !status.success() {
120        bail!("Server exited with non-zero status");
121    }
122
123    Ok(())
124}
125
126/// Run a binary directly.
127fn run_binary(args: &DevArgs) -> Result<()> {
128    println!("Starting MCP server...");
129    println!("  Binary: {}", args.path.display());
130    println!();
131
132    let mut cmd = Command::new(&args.path);
133    cmd.args(&args.server_args)
134        .stdin(Stdio::inherit())
135        .stdout(Stdio::inherit())
136        .stderr(Stdio::inherit());
137
138    let status = cmd.status().context("Failed to run server binary")?;
139
140    if !status.success() {
141        bail!("Server exited with non-zero status");
142    }
143
144    Ok(())
145}
146
147/// Build the cargo run arguments string for cargo-watch.
148fn build_cargo_run_args(args: &DevArgs) -> String {
149    let mut cmd_args = vec!["run".to_string()];
150
151    if args.release {
152        cmd_args.push("--release".to_string());
153    }
154
155    if !args.server_args.is_empty() {
156        cmd_args.push("--".to_string());
157        cmd_args.extend(args.server_args.clone());
158    }
159
160    cmd_args.join(" ")
161}
162
163/// Check if a command is available in PATH.
164fn is_command_available(cmd: &str) -> bool {
165    Command::new("which")
166        .arg(cmd)
167        .stdout(Stdio::null())
168        .stderr(Stdio::null())
169        .status()
170        .map(|s| s.success())
171        .unwrap_or(false)
172}
173
174/// Check if a path is an executable file.
175fn is_executable(path: &Path) -> bool {
176    #[cfg(unix)]
177    {
178        use std::os::unix::fs::PermissionsExt;
179        path.metadata()
180            .map(|m| m.is_file() && (m.permissions().mode() & 0o111) != 0)
181            .unwrap_or(false)
182    }
183
184    #[cfg(not(unix))]
185    {
186        path.is_file()
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    use std::path::PathBuf;
194
195    #[test]
196    fn test_build_cargo_run_args_basic() {
197        let args = DevArgs {
198            path: PathBuf::from("."),
199            watch: false,
200            server_args: vec![],
201            release: false,
202            inspector: false,
203            inspector_port: 5173,
204        };
205
206        assert_eq!(build_cargo_run_args(&args), "run");
207    }
208
209    #[test]
210    fn test_build_cargo_run_args_release() {
211        let args = DevArgs {
212            path: PathBuf::from("."),
213            watch: false,
214            server_args: vec![],
215            release: true,
216            inspector: false,
217            inspector_port: 5173,
218        };
219
220        assert_eq!(build_cargo_run_args(&args), "run --release");
221    }
222
223    #[test]
224    fn test_build_cargo_run_args_with_server_args() {
225        let args = DevArgs {
226            path: PathBuf::from("."),
227            watch: false,
228            server_args: vec!["--port".to_string(), "8080".to_string()],
229            release: false,
230            inspector: false,
231            inspector_port: 5173,
232        };
233
234        assert_eq!(build_cargo_run_args(&args), "run -- --port 8080");
235    }
236}