rust_docs_mcp/deps/
mod.rs

1pub mod outputs;
2pub mod tools;
3
4use rmcp::schemars;
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7
8/// Response for dependency information
9#[derive(Debug, Serialize, Deserialize, JsonSchema)]
10pub struct DependencyInfo {
11    /// The crate name and version being queried
12    pub crate_info: CrateIdentifier,
13
14    /// Direct dependencies of the crate
15    pub direct_dependencies: Vec<Dependency>,
16
17    /// Full dependency tree (only included if requested)
18    pub dependency_tree: Option<serde_json::Value>,
19
20    /// Total number of dependencies (direct + transitive)
21    pub total_dependencies: usize,
22}
23
24/// Identifies a crate with name and version
25#[derive(Debug, Serialize, Deserialize, JsonSchema)]
26pub struct CrateIdentifier {
27    pub name: String,
28    pub version: String,
29}
30
31/// Information about a single dependency
32#[derive(Debug, Serialize, Deserialize, JsonSchema)]
33pub struct Dependency {
34    /// Name of the dependency
35    pub name: String,
36
37    /// Version requirement specified in Cargo.toml
38    pub version_req: String,
39
40    /// Actual resolved version
41    pub resolved_version: Option<String>,
42
43    /// Kind of dependency (normal, dev, build)
44    pub kind: String,
45
46    /// Whether this is an optional dependency
47    pub optional: bool,
48
49    /// Features enabled for this dependency
50    pub features: Vec<String>,
51
52    /// Target platform (if dependency is platform-specific)
53    pub target: Option<String>,
54}
55
56/// Process cargo metadata output to extract dependency information
57pub fn process_cargo_metadata(
58    metadata: &serde_json::Value,
59    crate_name: &str,
60    crate_version: &str,
61    include_tree: bool,
62    filter: Option<&str>,
63) -> anyhow::Result<DependencyInfo> {
64    // Find the package in the metadata
65    let packages = metadata["packages"]
66        .as_array()
67        .ok_or_else(|| anyhow::anyhow!("No packages found in metadata"))?;
68
69    let package = packages
70        .iter()
71        .find(|p| {
72            p["name"].as_str() == Some(crate_name) && p["version"].as_str() == Some(crate_version)
73        })
74        .ok_or_else(|| {
75            anyhow::anyhow!(
76                "Package {}-{} not found in metadata",
77                crate_name,
78                crate_version
79            )
80        })?;
81
82    // Extract direct dependencies
83    let mut direct_dependencies = Vec::new();
84
85    if let Some(deps) = package["dependencies"].as_array() {
86        for dep in deps {
87            let name = dep["name"].as_str().unwrap_or_default();
88
89            // Apply filter if provided (case-insensitive)
90            if let Some(filter_str) = filter
91                && !name.to_lowercase().contains(&filter_str.to_lowercase())
92            {
93                continue;
94            }
95
96            // Find resolved version from the resolve section
97            let resolved_version = find_resolved_version(metadata, crate_name, crate_version, name);
98
99            direct_dependencies.push(Dependency {
100                name: name.to_string(),
101                version_req: dep["req"].as_str().unwrap_or_default().to_string(),
102                resolved_version,
103                kind: dep["kind"].as_str().unwrap_or("normal").to_string(),
104                optional: dep["optional"].as_bool().unwrap_or(false),
105                features: dep["features"]
106                    .as_array()
107                    .map(|arr| {
108                        arr.iter()
109                            .filter_map(|v| v.as_str().map(String::from))
110                            .collect()
111                    })
112                    .unwrap_or_default(),
113                target: dep["target"].as_str().map(String::from),
114            });
115        }
116    }
117
118    // Count total dependencies
119    let total_dependencies = if let Some(resolve) = metadata["resolve"].as_object() {
120        if let Some(nodes) = resolve["nodes"].as_array() {
121            // Find the node for our package and count its dependencies
122            nodes
123                .iter()
124                .find(|n| {
125                    n["id"]
126                        .as_str()
127                        .map(|id| id.starts_with(&format!("{crate_name} {crate_version}")))
128                        .unwrap_or(false)
129                })
130                .and_then(|n| n["dependencies"].as_array())
131                .map(|deps| deps.len())
132                .unwrap_or(0)
133        } else {
134            direct_dependencies.len()
135        }
136    } else {
137        direct_dependencies.len()
138    };
139
140    Ok(DependencyInfo {
141        crate_info: CrateIdentifier {
142            name: crate_name.to_string(),
143            version: crate_version.to_string(),
144        },
145        direct_dependencies,
146        dependency_tree: if include_tree {
147            Some(metadata["resolve"].clone())
148        } else {
149            None
150        },
151        total_dependencies,
152    })
153}
154
155/// Find the resolved version of a dependency from the resolve section
156fn find_resolved_version(
157    metadata: &serde_json::Value,
158    parent_name: &str,
159    parent_version: &str,
160    dep_name: &str,
161) -> Option<String> {
162    let resolve = metadata["resolve"].as_object()?;
163    let nodes = resolve["nodes"].as_array()?;
164
165    // Find the parent node
166    let parent_node = nodes.iter().find(|n| {
167        n["id"]
168            .as_str()
169            .map(|id| id.starts_with(&format!("{parent_name} {parent_version}")))
170            .unwrap_or(false)
171    })?;
172
173    // Find the dependency in the parent's deps
174    let deps = parent_node["deps"].as_array()?;
175    for dep in deps {
176        if dep["name"].as_str() == Some(dep_name) {
177            // Extract version from the pkg field
178            if let Some(pkg) = dep["pkg"].as_str() {
179                // pkg format is "name version (source)"
180                let parts: Vec<&str> = pkg.split(' ').collect();
181                if parts.len() >= 2 {
182                    return Some(parts[1].to_string());
183                }
184            }
185        }
186    }
187
188    None
189}