use std::process::Command;
use rmcp::ErrorData;
use crate::{
Tool, execute_command,
serde_utils::{deserialize_string, deserialize_string_vec, locking_mode_to_cli_flags},
};
fn kind_to_cli_flag(kind: Option<&str>) -> Result<Option<&'static str>, ErrorData> {
Ok(match kind {
None => None,
Some("lib") => Some("--lib"),
Some("bin") => Some("--bin"),
Some("example") => Some("--example"),
Some("test") => Some("--test"),
Some("bench") => Some("--bench"),
Some(unknown) => {
return Err(ErrorData::invalid_params(
format!(
"Unknown kind: {unknown}. Valid options are: lib, bin, example, test, bench"
),
None,
));
}
})
}
#[derive(Debug, ::serde::Deserialize, ::schemars::JsonSchema)]
pub struct CargoExpandRequest {
#[serde(default, deserialize_with = "deserialize_string")]
toolchain: Option<String>,
#[serde(default, deserialize_with = "deserialize_string")]
item: Option<String>,
#[serde(default, deserialize_with = "deserialize_string")]
package: Option<String>,
#[serde(default, deserialize_with = "deserialize_string")]
kind: Option<String>,
#[serde(default, deserialize_with = "deserialize_string")]
name: Option<String>,
#[serde(default)]
tests: 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, deserialize_with = "deserialize_string")]
profile: Option<String>,
#[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)]
ugly: Option<bool>,
#[serde(default)]
verbose: Option<bool>,
#[serde(default, deserialize_with = "deserialize_string")]
locking_mode: Option<String>,
}
impl CargoExpandRequest {
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("expand");
if let Some(package) = &self.package {
cmd.arg("--package").arg(package);
}
if let Some(flag) = kind_to_cli_flag(self.kind.as_deref())? {
cmd.arg(flag);
if let Some(name) = &self.name {
if self.kind.as_deref() == Some("lib") {
return Err(ErrorData::invalid_params(
"name cannot be specified when kind is \"lib\"",
None,
));
}
cmd.arg(name);
}
}
if self.tests.unwrap_or(false) {
cmd.arg("--tests");
}
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 let Some(profile) = &self.profile {
cmd.arg("--profile").arg(profile);
}
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);
}
let locking_flags = locking_mode_to_cli_flags(self.locking_mode.as_deref(), "locked")?;
cmd.args(locking_flags);
if self.ugly.unwrap_or(false) {
cmd.arg("--ugly");
}
if self.verbose.unwrap_or(false) {
cmd.arg("--verbose");
}
if let Some(item) = &self.item {
cmd.arg(item);
}
Ok(cmd)
}
}
pub struct CargoExpandRmcpTool;
impl Tool for CargoExpandRmcpTool {
const NAME: &'static str = "cargo-expand";
const TITLE: &'static str = "Cargo Expand";
const DESCRIPTION: &'static str = "Show the result of macro expansion for Rust code. Requires cargo-expand to be installed (cargo install cargo-expand). Useful for debugging procedural macros, derive macros, and understanding what code macros generate.";
type RequestArgs = CargoExpandRequest;
fn call_rmcp_tool(&self, req: Self::RequestArgs) -> Result<crate::Response, rmcp::ErrorData> {
execute_command(req.build_cmd()?, Self::NAME).map(Into::into)
}
}