use std::process::Command;
use crate::{
Tool, execute_command,
serde_utils::{
deserialize_string, deserialize_string_vec, locking_mode_to_cli_flags,
output_verbosity_to_cli_flags,
},
tools::cargo::CargoWorkspaceInfoRmcpTool,
};
use rmcp::ErrorData;
#[derive(Debug, ::serde::Deserialize, ::schemars::JsonSchema)]
pub struct CargoMetadataRequest {
#[serde(default, deserialize_with = "deserialize_string")]
toolchain: Option<String>,
#[serde(default, deserialize_with = "deserialize_string")]
filter_platform: Option<String>,
#[serde(default)]
no_deps: Option<bool>,
#[serde(default, deserialize_with = "deserialize_string")]
output_verbosity: Option<String>,
#[serde(default, deserialize_with = "deserialize_string")]
config: 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")]
manifest_path: Option<String>,
#[serde(default, deserialize_with = "deserialize_string")]
lockfile_path: Option<String>,
#[serde(default, deserialize_with = "deserialize_string")]
locking_mode: Option<String>,
}
impl CargoMetadataRequest {
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("metadata");
cmd.arg("--format-version").arg("1");
if let Some(triple) = &self.filter_platform {
cmd.arg("--filter-platform").arg(triple);
}
if self.no_deps.unwrap_or(false) {
cmd.arg("--no-deps");
}
let output_flags = output_verbosity_to_cli_flags(self.output_verbosity.as_deref())?;
cmd.args(output_flags);
if let Some(config) = &self.config {
cmd.arg("--config").arg(config);
}
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(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);
}
let locking_flags = locking_mode_to_cli_flags(self.locking_mode.as_deref(), "locked")?;
cmd.args(locking_flags);
Ok(cmd)
}
}
pub struct CargoMetadataRmcpTool;
impl Tool for CargoMetadataRmcpTool {
const NAME: &'static str = "cargo-metadata";
const TITLE: &'static str = "cargo metadata";
const DESCRIPTION: &'static str = "Outputs a listing of a project's resolved dependencies and metadata in machine-readable format (JSON).";
type RequestArgs = CargoMetadataRequest;
fn call_rmcp_tool(&self, request: Self::RequestArgs) -> Result<crate::Response, ErrorData> {
let cmd = request.build_cmd()?;
let mut response: crate::Response = execute_command(cmd, Self::NAME)?.into();
if !request.no_deps.unwrap_or(false) {
response.add_recommendation(
"Set no_deps=true to return only workspace member metadata, reducing output size and token usage",
);
}
response.add_recommendation(format!(
"Use #{} if you don't need full metadata",
CargoWorkspaceInfoRmcpTool::NAME
));
Ok(response)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_deserialize_with_features_array() {
let input = json!({
"features": ["serde", "tokio"],
});
let tool: Result<CargoMetadataRequest, _> = serde_json::from_value(input);
let tool = tool.expect("Deserialization should succeed with features array");
assert_eq!(
tool.features.unwrap(),
["serde".to_owned(), "tokio".to_owned()]
);
}
#[test]
fn test_deserialize_with_single_feature_string() {
let input = json!({
"features": "serde",
});
let tool: Result<CargoMetadataRequest, _> = serde_json::from_value(input);
let tool = tool.expect("Deserialization should succeed with single feature string");
assert_eq!(tool.features.unwrap(), ["serde".to_owned()]);
}
#[test]
fn test_deserialize_with_features_string_array() {
let input = json!({
"features": "[\"serde\",\"tokio\"]",
});
let tool: Result<CargoMetadataRequest, _> = serde_json::from_value(input);
let tool = tool
.expect("Deserialization should succeed with features string that looks like array");
assert_eq!(tool.features.unwrap(), ["[\"serde\",\"tokio\"]".to_owned()]);
}
}