use std::process::Command;
use crate::{
Response, Tool, execute_command,
serde_utils::{
deserialize_string, deserialize_string_vec, locking_mode_to_cli_flags,
output_verbosity_to_cli_flags,
},
tools::cargo::CargoCheckRmcpTool,
};
use rmcp::ErrorData;
#[derive(Debug, ::serde::Deserialize, ::schemars::JsonSchema)]
pub struct CargoBuildRequest {
#[serde(default, deserialize_with = "deserialize_string")]
toolchain: Option<String>,
#[serde(default, deserialize_with = "deserialize_string_vec")]
package: Option<Vec<String>>,
#[serde(default)]
workspace: Option<bool>,
#[serde(default, deserialize_with = "deserialize_string_vec")]
exclude: Option<Vec<String>>,
#[serde(default)]
lib: Option<bool>,
#[serde(default)]
bins: Option<bool>,
#[serde(default, deserialize_with = "deserialize_string")]
bin: Option<String>,
#[serde(default)]
examples: Option<bool>,
#[serde(default, deserialize_with = "deserialize_string")]
example: Option<String>,
#[serde(default)]
tests: Option<bool>,
#[serde(default, deserialize_with = "deserialize_string")]
test: Option<String>,
#[serde(default)]
benches: Option<bool>,
#[serde(default, deserialize_with = "deserialize_string")]
bench: Option<String>,
#[serde(default)]
all_targets: Option<bool>,
#[serde(default, deserialize_with = "deserialize_string_vec")]
features: Option<Vec<String>>,
#[serde(default)]
all_features: Option<bool>,
#[serde(default)]
no_default_features: Option<bool>,
#[serde(default)]
release: Option<bool>,
#[serde(default, deserialize_with = "deserialize_string")]
profile: Option<String>,
#[serde(default)]
jobs: Option<u32>,
#[serde(default)]
keep_going: Option<bool>,
#[serde(default, deserialize_with = "deserialize_string")]
target: Option<String>,
#[serde(default, deserialize_with = "deserialize_string")]
target_dir: Option<String>,
#[serde(default, deserialize_with = "deserialize_string")]
manifest_path: Option<String>,
#[serde(default, deserialize_with = "deserialize_string")]
lockfile_path: Option<String>,
#[serde(default)]
ignore_rust_version: Option<bool>,
#[serde(default, deserialize_with = "deserialize_string")]
locking_mode: Option<String>,
#[serde(default, deserialize_with = "deserialize_string")]
output_verbosity: Option<String>,
#[serde(default)]
warnings_as_errors: Option<bool>,
}
impl CargoBuildRequest {
pub fn build_cmd(&self) -> Result<Command, ErrorData> {
let mut cmd = Command::new("cargo");
if let Some(toolchain) = &self.toolchain {
cmd.arg(format!("+{toolchain}"));
}
cmd.arg("build");
if let Some(packages) = &self.package {
for package in packages {
cmd.arg("--package").arg(package);
}
}
if self.workspace.unwrap_or(false) {
cmd.arg("--workspace");
}
if let Some(excludes) = &self.exclude {
for exclude in excludes {
cmd.arg("--exclude").arg(exclude);
}
}
if self.lib.unwrap_or(false) {
cmd.arg("--lib");
}
if self.bins.unwrap_or(false) {
cmd.arg("--bins");
}
if let Some(bin) = &self.bin {
cmd.arg("--bin").arg(bin);
}
if self.examples.unwrap_or(false) {
cmd.arg("--examples");
}
if let Some(example) = &self.example {
cmd.arg("--example").arg(example);
}
if self.tests.unwrap_or(false) {
cmd.arg("--tests");
}
if let Some(test) = &self.test {
cmd.arg("--test").arg(test);
}
if self.benches.unwrap_or(false) {
cmd.arg("--benches");
}
if let Some(bench) = &self.bench {
cmd.arg("--bench").arg(bench);
}
if self.all_targets.unwrap_or(false) {
cmd.arg("--all-targets");
}
if let Some(features) = &self.features {
cmd.arg("--features").arg(features.join(","));
}
if self.all_features.unwrap_or(false) {
cmd.arg("--all-features");
}
if self.no_default_features.unwrap_or(false) {
cmd.arg("--no-default-features");
}
if self.release.unwrap_or(false) {
cmd.arg("--release");
}
if let Some(profile) = &self.profile {
cmd.arg("--profile").arg(profile);
}
if let Some(jobs) = self.jobs {
cmd.arg("--jobs").arg(jobs.to_string());
}
if self.keep_going.unwrap_or(false) {
cmd.arg("--keep-going");
}
if let Some(target) = &self.target {
cmd.arg("--target").arg(target);
}
if let Some(target_dir) = &self.target_dir {
cmd.arg("--target-dir").arg(target_dir);
}
if let Some(manifest_path) = &self.manifest_path {
cmd.arg("--manifest-path").arg(manifest_path);
}
if let Some(lockfile_path) = &self.lockfile_path {
cmd.arg("--lockfile-path").arg(lockfile_path);
}
if self.ignore_rust_version.unwrap_or(false) {
cmd.arg("--ignore-rust-version");
}
let locking_flags = locking_mode_to_cli_flags(self.locking_mode.as_deref(), "locked")?;
for flag in locking_flags {
cmd.arg(flag);
}
let output_flags = output_verbosity_to_cli_flags(self.output_verbosity.as_deref())?;
cmd.args(output_flags);
if self.warnings_as_errors.unwrap_or(false) {
cmd.env("RUSTFLAGS", "-D warnings");
}
Ok(cmd)
}
}
pub struct CargoBuildRmcpTool;
impl Tool for CargoBuildRmcpTool {
const NAME: &'static str = "cargo-build";
const TITLE: &'static str = "cargo build";
const DESCRIPTION: &'static str =
"Builds a Rust project using Cargo. Usually, run without any additional arguments.";
type RequestArgs = CargoBuildRequest;
fn call_rmcp_tool(&self, request: Self::RequestArgs) -> Result<crate::Response, ErrorData> {
let cmd = request.build_cmd()?;
let start_time = std::time::Instant::now();
let output = execute_command(cmd, Self::NAME)?;
let duration = start_time.elapsed();
let mut response: Response = output.into();
if duration.as_secs() >= 60 {
response.add_recommendation(format!(
"Consider using #{} tool for faster feedback",
CargoCheckRmcpTool::NAME
));
}
Ok(response)
}
}