pub mod outputs;
pub mod tools;
use rmcp::schemars;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct DependencyInfo {
pub crate_info: CrateIdentifier,
pub direct_dependencies: Vec<Dependency>,
pub dependency_tree: Option<serde_json::Value>,
pub total_dependencies: usize,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CrateIdentifier {
pub name: String,
pub version: String,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct Dependency {
pub name: String,
pub version_req: String,
pub resolved_version: Option<String>,
pub kind: String,
pub optional: bool,
pub features: Vec<String>,
pub target: Option<String>,
}
pub fn process_cargo_metadata(
metadata: &serde_json::Value,
crate_name: &str,
crate_version: &str,
include_tree: bool,
filter: Option<&str>,
) -> anyhow::Result<DependencyInfo> {
let packages = metadata["packages"]
.as_array()
.ok_or_else(|| anyhow::anyhow!("No packages found in metadata"))?;
let package = packages
.iter()
.find(|p| {
p["name"].as_str() == Some(crate_name) && p["version"].as_str() == Some(crate_version)
})
.ok_or_else(|| {
anyhow::anyhow!(
"Package {}-{} not found in metadata",
crate_name,
crate_version
)
})?;
let mut direct_dependencies = Vec::new();
if let Some(deps) = package["dependencies"].as_array() {
for dep in deps {
let name = dep["name"].as_str().unwrap_or_default();
if let Some(filter_str) = filter
&& !name.to_lowercase().contains(&filter_str.to_lowercase())
{
continue;
}
let resolved_version = find_resolved_version(metadata, crate_name, crate_version, name);
direct_dependencies.push(Dependency {
name: name.to_string(),
version_req: dep["req"].as_str().unwrap_or_default().to_string(),
resolved_version,
kind: dep["kind"].as_str().unwrap_or("normal").to_string(),
optional: dep["optional"].as_bool().unwrap_or(false),
features: dep["features"]
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default(),
target: dep["target"].as_str().map(String::from),
});
}
}
let total_dependencies = if let Some(resolve) = metadata["resolve"].as_object() {
if let Some(nodes) = resolve["nodes"].as_array() {
nodes
.iter()
.find(|n| {
n["id"]
.as_str()
.map(|id| id.starts_with(&format!("{crate_name} {crate_version}")))
.unwrap_or(false)
})
.and_then(|n| n["dependencies"].as_array())
.map(|deps| deps.len())
.unwrap_or(0)
} else {
direct_dependencies.len()
}
} else {
direct_dependencies.len()
};
Ok(DependencyInfo {
crate_info: CrateIdentifier {
name: crate_name.to_string(),
version: crate_version.to_string(),
},
direct_dependencies,
dependency_tree: if include_tree {
Some(metadata["resolve"].clone())
} else {
None
},
total_dependencies,
})
}
fn find_resolved_version(
metadata: &serde_json::Value,
parent_name: &str,
parent_version: &str,
dep_name: &str,
) -> Option<String> {
let resolve = metadata["resolve"].as_object()?;
let nodes = resolve["nodes"].as_array()?;
let parent_node = nodes.iter().find(|n| {
n["id"]
.as_str()
.map(|id| id.starts_with(&format!("{parent_name} {parent_version}")))
.unwrap_or(false)
})?;
let deps = parent_node["deps"].as_array()?;
for dep in deps {
if dep["name"].as_str() == Some(dep_name) {
if let Some(pkg) = dep["pkg"].as_str() {
let parts: Vec<&str> = pkg.split(' ').collect();
if parts.len() >= 2 {
return Some(parts[1].to_string());
}
}
}
}
None
}