use std::process::Command;
use crate::{
Tool, execute_command,
response::Response,
serde_utils::{deserialize_string, deserialize_string_vec, locking_mode_to_cli_flags},
};
use rmcp::ErrorData;
#[derive(Debug, ::serde::Deserialize, ::schemars::JsonSchema)]
pub struct CargoTreeRequest {
#[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, deserialize_with = "deserialize_string_vec")]
edges: Option<Vec<String>>,
#[serde(default, deserialize_with = "deserialize_string")]
invert: Option<String>,
#[serde(default, deserialize_with = "deserialize_string_vec")]
prune: Option<Vec<String>>,
#[serde(default)]
depth: Option<u32>,
#[serde(default, deserialize_with = "deserialize_string")]
prefix: Option<String>,
#[serde(default)]
no_dedupe: Option<bool>,
#[serde(default)]
duplicates: Option<bool>,
#[serde(default, deserialize_with = "deserialize_string")]
format: Option<String>,
#[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")]
target: Option<String>,
#[serde(default, deserialize_with = "deserialize_string")]
manifest_path: Option<String>,
#[serde(default, deserialize_with = "deserialize_string")]
locking_mode: Option<String>,
}
impl CargoTreeRequest {
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("tree");
cmd.arg("--charset").arg("ascii");
if let Some(packages) = &self.package {
for pkg in packages {
cmd.arg("--package").arg(pkg);
}
}
if self.workspace.unwrap_or(false) {
cmd.arg("--workspace");
}
if let Some(excludes) = &self.exclude {
for exc in excludes {
cmd.arg("--exclude").arg(exc);
}
}
if let Some(edges) = &self.edges {
cmd.arg("--edges").arg(edges.join(","));
}
if let Some(invert) = &self.invert {
cmd.arg("--invert").arg(invert);
}
if let Some(prunes) = &self.prune {
for p in prunes {
cmd.arg("--prune").arg(p);
}
}
if let Some(depth) = self.depth {
cmd.arg("--depth").arg(depth.to_string());
}
if let Some(prefix) = &self.prefix {
cmd.arg("--prefix").arg(prefix);
}
if self.no_dedupe.unwrap_or(false) {
cmd.arg("--no-dedupe");
}
if self.duplicates.unwrap_or(false) {
cmd.arg("--duplicates");
}
if let Some(format) = &self.format {
cmd.arg("--format").arg(format);
}
if let Some(features) = &self.features
&& !features.is_empty()
{
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(target) = &self.target {
cmd.arg("--target").arg(target);
}
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);
Ok(cmd)
}
}
pub struct CargoTreeRmcpTool;
impl Tool for CargoTreeRmcpTool {
const NAME: &'static str = "cargo-tree";
const TITLE: &'static str = "cargo tree";
const DESCRIPTION: &'static str = "Display a tree visualization of a dependency graph. Useful for understanding dependency relationships, finding duplicate dependencies, and debugging dependency resolution issues.";
type RequestArgs = CargoTreeRequest;
fn call_rmcp_tool(&self, request: Self::RequestArgs) -> Result<crate::Response, ErrorData> {
let cmd = request.build_cmd()?;
let output = execute_command(cmd, Self::NAME)?;
let stdout_len = if output.success()
&& let Some(stdout) = &output.stdout
{
stdout.0.len()
} else {
0
};
let mut response: Response = output.into();
if stdout_len > 16384 && request.depth.is_none() && request.duplicates.is_none() {
response.add_recommendation(
"Use depth parameter to limit output size for large dependency trees",
);
}
Ok(response)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_minimal_request() {
let input = json!({});
let request: CargoTreeRequest =
serde_json::from_value(input).expect("Should deserialize empty request");
let cmd = request.build_cmd().expect("Should build command");
let args: Vec<_> = cmd.get_args().map(|s| s.to_str().unwrap()).collect();
assert_eq!(args, vec!["tree", "--charset", "ascii", "--locked"]);
}
#[test]
fn test_with_package() {
let input = json!({
"package": ["my-crate"]
});
let request: CargoTreeRequest =
serde_json::from_value(input).expect("Should deserialize request with package");
let cmd = request.build_cmd().expect("Should build command");
let args: Vec<_> = cmd.get_args().map(|s| s.to_str().unwrap()).collect();
assert_eq!(
args,
vec![
"tree",
"--charset",
"ascii",
"--package",
"my-crate",
"--locked"
]
);
}
#[test]
fn test_with_invert() {
let input = json!({
"invert": "tokio"
});
let request: CargoTreeRequest =
serde_json::from_value(input).expect("Should deserialize request with invert");
let cmd = request.build_cmd().expect("Should build command");
let args: Vec<_> = cmd.get_args().map(|s| s.to_str().unwrap()).collect();
assert_eq!(
args,
vec![
"tree",
"--charset",
"ascii",
"--invert",
"tokio",
"--locked"
]
);
}
#[test]
fn test_with_depth() {
let input = json!({
"depth": 3
});
let request: CargoTreeRequest =
serde_json::from_value(input).expect("Should deserialize request with depth");
let cmd = request.build_cmd().expect("Should build command");
let args: Vec<_> = cmd.get_args().map(|s| s.to_str().unwrap()).collect();
assert_eq!(
args,
vec!["tree", "--charset", "ascii", "--depth", "3", "--locked"]
);
}
#[test]
fn test_with_duplicates() {
let input = json!({
"duplicates": true
});
let request: CargoTreeRequest =
serde_json::from_value(input).expect("Should deserialize request with duplicates");
let cmd = request.build_cmd().expect("Should build command");
let args: Vec<_> = cmd.get_args().map(|s| s.to_str().unwrap()).collect();
assert_eq!(
args,
vec!["tree", "--charset", "ascii", "--duplicates", "--locked"]
);
}
#[test]
fn test_with_edges() {
let input = json!({
"edges": ["normal", "build"]
});
let request: CargoTreeRequest =
serde_json::from_value(input).expect("Should deserialize request with edges");
let cmd = request.build_cmd().expect("Should build command");
let args: Vec<_> = cmd.get_args().map(|s| s.to_str().unwrap()).collect();
assert_eq!(
args,
vec![
"tree",
"--charset",
"ascii",
"--edges",
"normal,build",
"--locked"
]
);
}
#[test]
fn test_with_toolchain() {
let input = json!({
"toolchain": "nightly"
});
let request: CargoTreeRequest =
serde_json::from_value(input).expect("Should deserialize request with toolchain");
let cmd = request.build_cmd().expect("Should build command");
let args: Vec<_> = cmd.get_args().map(|s| s.to_str().unwrap()).collect();
assert_eq!(
args,
vec!["+nightly", "tree", "--charset", "ascii", "--locked"]
);
}
#[test]
fn test_with_features() {
let input = json!({
"features": ["serde", "tokio"],
"all_features": false,
"no_default_features": true
});
let request: CargoTreeRequest =
serde_json::from_value(input).expect("Should deserialize request with features");
let cmd = request.build_cmd().expect("Should build command");
let args: Vec<_> = cmd.get_args().map(|s| s.to_str().unwrap()).collect();
assert_eq!(
args,
vec![
"tree",
"--charset",
"ascii",
"--features",
"serde,tokio",
"--no-default-features",
"--locked"
]
);
}
#[test]
fn test_with_format() {
let input = json!({
"format": "{p} {l}"
});
let request: CargoTreeRequest =
serde_json::from_value(input).expect("Should deserialize request with format");
let cmd = request.build_cmd().expect("Should build command");
let args: Vec<_> = cmd.get_args().map(|s| s.to_str().unwrap()).collect();
assert_eq!(
args,
vec![
"tree",
"--charset",
"ascii",
"--format",
"{p} {l}",
"--locked"
]
);
}
}