Skip to content
Open
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,12 @@ ones released yesterday.
- **For cratesio**: Provide `version` (e.g., `{crate_name: "serde", source_type: "cratesio", version: "1.0.215"}`)
- **For github**: Provide `github_url` and either `branch` OR `tag` (e.g., `{crate_name: "my-crate", source_type: "github", github_url: "https://github.com/user/repo", tag: "v1.0.0"}`)
- **For local**: Provide `path`, optional `version` (e.g., `{crate_name: "my-crate", source_type: "local", path: "~/projects/my-crate"}`)
- **Optional `features`**: Specific features to enable instead of `--all-features`. Use for crates with mutually exclusive features (e.g., `{crate_name: "leptos-use", source_type: "cratesio", version: "0.15.8", features: ["axum"]}`)
- `remove_crate` - Remove cached crate versions to free disk space
- `list_cached_crates` - View all cached crates with versions and sizes
- `list_crate_versions` - List cached versions for a specific crate
- `get_crates_metadata` - Batch metadata queries for multiple crates
- `cache_operations` - Manage and monitor background caching operations (list, status, cancel, clear)

### Documentation Queries

Expand Down
5 changes: 2 additions & 3 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -192,13 +192,12 @@ main() {
echo -e "${BLUE}(~/.claude/settings.json or your project/local settings)${NC}"
echo
echo -e "${GREEN}
\"mcp__rust-docs__cache_crate_from_cratesio\",
\"mcp__rust-docs__cache_crate_from_github\",
\"mcp__rust-docs__cache_crate_from_local\",
\"mcp__rust-docs__cache_crate\",
\"mcp__rust-docs__remove_crate\",
\"mcp__rust-docs__list_cached_crates\",
\"mcp__rust-docs__list_crate_versions\",
\"mcp__rust-docs__get_crates_metadata\",
\"mcp__rust-docs__cache_operations\",
\"mcp__rust-docs__list_crate_items\",
\"mcp__rust-docs__search_items\",
\"mcp__rust-docs__search_items_preview\",
Expand Down
2 changes: 1 addition & 1 deletion rust-docs-mcp/benches/indexing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ impl BenchFixture {
eprintln!("Fixture not found; generating (this may take several minutes)...");
runtime.block_on(async {
cache
.ensure_crate_docs(&name, &version, None)
.ensure_crate_docs(&name, &version, None, None)
.await
.expect("ensure_crate_docs");
});
Expand Down
5 changes: 4 additions & 1 deletion rust-docs-mcp/src/cache/docgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ impl DocGenerator {
name: &str,
version: &str,
progress_callback: Option<ProgressCallback>,
features: Option<Vec<String>>,
) -> Result<PathBuf> {
tracing::info!(
"DocGenerator::generate_docs starting for {}-{}",
Expand Down Expand Up @@ -146,7 +147,7 @@ impl DocGenerator {
}

// Run cargo rustdoc with JSON output using unified function
rustdoc::run_cargo_rustdoc_json(&source_path, None, None).await?;
rustdoc::run_cargo_rustdoc_json(&source_path, None, None, features).await?;

// Rustdoc complete - report 70%
if let Some(ref callback) = progress_callback {
Expand Down Expand Up @@ -199,6 +200,7 @@ impl DocGenerator {
version: &str,
member_path: &str,
progress_callback: Option<ProgressCallback>,
features: Option<Vec<String>>,
) -> Result<PathBuf> {
let source_path = self.storage.source_path(name, version)?;
let member_full_path = source_path.join(member_path);
Expand Down Expand Up @@ -248,6 +250,7 @@ impl DocGenerator {
&source_path,
Some(&package_name),
Some(&member_target_dir),
features,
)
.await?;

Expand Down
57 changes: 41 additions & 16 deletions rust-docs-mcp/src/cache/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ use std::sync::Arc;

/// Cache key for the in-memory LRU of parsed `rustdoc_types::Crate` objects.
type DocsCacheKey = (String, String, Option<String>);
type SourceParams = (
String,
String,
Option<Vec<String>>,
Option<String>,
bool,
Option<Vec<String>>,
);

/// Service for managing crate caching and documentation generation
pub struct CrateCache {
Expand Down Expand Up @@ -63,6 +71,7 @@ impl CrateCache {
name: &str,
version: &str,
source: Option<&str>,
features: Option<Vec<String>>,
) -> Result<Arc<rustdoc_types::Crate>> {
tracing::info!("ensure_crate_docs called for {}-{}", name, version);

Expand Down Expand Up @@ -138,7 +147,7 @@ impl CrateCache {
// Note: progress_callback is None here because this method is called from
// various places. The progress-aware path goes through cache_crate_with_source
// which passes progress callbacks directly to generate_docs.
match self.generate_docs(name, version, None).await {
match self.generate_docs(name, version, None, features).await {
Ok(_) => {
// Load and return the generated docs
self.load_docs(name, version, None).await
Expand All @@ -162,6 +171,7 @@ impl CrateCache {
version: &str,
source: Option<&str>,
member_path: &str,
features: Option<Vec<String>>,
) -> Result<Arc<rustdoc_types::Crate>> {
// Check if docs already exist for this member
if self.storage.has_docs(name, version, Some(member_path)) {
Expand All @@ -175,7 +185,7 @@ impl CrateCache {
}

// Generate documentation for the specific workspace member
self.generate_workspace_member_docs(name, version, member_path, None)
self.generate_workspace_member_docs(name, version, member_path, None, features)
.await?;

// Get package name for the member
Expand Down Expand Up @@ -216,7 +226,7 @@ impl CrateCache {
// If member is specified, use workspace member logic
if let Some(member_path) = member {
return self
.ensure_workspace_member_docs(name, version, None, member_path)
.ensure_workspace_member_docs(name, version, None, member_path, None)
.await;
}

Expand All @@ -240,7 +250,7 @@ impl CrateCache {
}

// Regular crate, use normal flow
self.ensure_crate_docs(name, version, None).await
self.ensure_crate_docs(name, version, None, None).await
}

/// Download or copy a crate based on source type
Expand All @@ -262,9 +272,10 @@ impl CrateCache {
name: &str,
version: &str,
progress_callback: Option<crate::cache::downloader::ProgressCallback>,
features: Option<Vec<String>>,
) -> Result<PathBuf> {
self.doc_generator
.generate_docs(name, version, progress_callback)
.generate_docs(name, version, progress_callback, features)
.await
}

Expand All @@ -275,9 +286,10 @@ impl CrateCache {
version: &str,
member_path: &str,
progress_callback: Option<crate::cache::downloader::ProgressCallback>,
features: Option<Vec<String>>,
) -> Result<PathBuf> {
self.doc_generator
.generate_workspace_member_docs(name, version, member_path, progress_callback)
.generate_workspace_member_docs(name, version, member_path, progress_callback, features)
.await
}

Expand Down Expand Up @@ -472,11 +484,12 @@ impl CrateCache {
members: &Option<Vec<String>>,
source_str: Option<&str>,
source: &CrateSource,
features: Option<Vec<String>>,
) -> Result<CacheResponse> {
// If members are specified, cache those specific workspace members
if let Some(members) = members {
let response = self
.cache_workspace_members(crate_name, version, members, source_str, true)
.cache_workspace_members(crate_name, version, members, source_str, features, true)
.await;

// Check if all failed for proper error handling
Expand Down Expand Up @@ -504,25 +517,23 @@ impl CrateCache {
Ok(self.generate_workspace_response(crate_name, version, members, source, true))
} else {
// Not a workspace, proceed with normal caching
self.ensure_crate_docs(crate_name, version, source_str)
self.ensure_crate_docs(crate_name, version, source_str, features)
.await?;

Ok(CacheResponse::success_updated(crate_name, version))
}
}

/// Extract source parameters from CrateSource enum
fn extract_source_params(
&self,
source: &CrateSource,
) -> (String, String, Option<Vec<String>>, Option<String>, bool) {
fn extract_source_params(&self, source: &CrateSource) -> SourceParams {
match source {
CrateSource::CratesIO(params) => (
params.crate_name.clone(),
params.version.clone(),
params.members.clone(),
None,
params.update.unwrap_or(false),
params.features.clone(),
),
CrateSource::GitHub(params) => {
let version = if let Some(branch) = &params.branch {
Expand All @@ -548,6 +559,7 @@ impl CrateCache {
params.members.clone(),
source_str,
params.update.unwrap_or(false),
params.features.clone(),
)
}
CrateSource::LocalPath(params) => (
Expand All @@ -559,6 +571,7 @@ impl CrateCache {
params.members.clone(),
Some(params.path.clone()),
params.update.unwrap_or(false),
params.features.clone(),
),
}
}
Expand All @@ -577,6 +590,7 @@ impl CrateCache {
version: &str,
members: &[String],
source_str: Option<&str>,
features: Option<Vec<String>>,
updated: bool,
) -> CacheResponse {
use futures::future::join_all;
Expand All @@ -596,6 +610,7 @@ impl CrateCache {
.iter()
.map(|member| {
let member_clone = member.clone();
let features = features.clone();
let sem = std::sync::Arc::clone(&sem);
async move {
let _permit = sem.acquire().await.expect("semaphore closed");
Expand All @@ -605,6 +620,7 @@ impl CrateCache {
version,
source_str,
&member_clone,
features,
)
.await;
(member_clone, result)
Expand Down Expand Up @@ -669,6 +685,7 @@ impl CrateCache {
members: &Option<Vec<String>>,
source_str: Option<&str>,
source: &CrateSource,
features: Option<Vec<String>>,
) -> String {
// Create transaction for safe update
let mut transaction = CacheTransaction::new(&self.storage, crate_name, version);
Expand All @@ -684,7 +701,9 @@ impl CrateCache {

// Try to re-cache the crate
let update_result = self
.cache_crate_with_update_impl(crate_name, version, members, source_str, source)
.cache_crate_with_update_impl(
crate_name, version, members, source_str, source, features,
)
.await;

// Check if update was successful
Expand Down Expand Up @@ -713,9 +732,10 @@ impl CrateCache {
version: &str,
members: &[String],
source_str: Option<&str>,
features: Option<Vec<String>>,
updated: bool,
) -> CacheResponse {
self.cache_workspace_members(crate_name, version, members, source_str, updated)
self.cache_workspace_members(crate_name, version, members, source_str, features, updated)
.await
}

Expand Down Expand Up @@ -807,7 +827,7 @@ impl CrateCache {
};

// Extract parameters from source
let (crate_name, version, members, source_str, update) =
let (crate_name, version, members, source_str, update, features) =
self.extract_source_params(&source);

tracing::info!(
Expand Down Expand Up @@ -836,6 +856,7 @@ impl CrateCache {
&members,
source_str.as_deref(),
&source,
features.clone(),
)
.await;
}
Expand All @@ -853,6 +874,7 @@ impl CrateCache {
&version,
&members,
source_str.as_deref(),
features.clone(),
false,
)
.await;
Expand Down Expand Up @@ -942,7 +964,10 @@ impl CrateCache {
tm.update_step(tid, 1, "Running cargo rustdoc").await;
}

match self.generate_docs(&crate_name, &version, None).await {
match self
.generate_docs(&crate_name, &version, None, features)
.await
{
Ok(_) => {
// Update to indexing stage
if let (Some(tm), Some(tid)) = (&task_manager, &task_id) {
Expand Down
19 changes: 19 additions & 0 deletions rust-docs-mcp/src/cache/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ pub struct CacheCrateParams {
description = "Force re-download and re-cache the crate even if it already exists. Defaults to false. The existing cache is preserved until the update succeeds."
)]
pub update: Option<bool>,
#[schemars(
description = "Specific features to enable instead of --all-features. Use this for crates with mutually exclusive features (e.g., leptos-use has conflicting 'actix' and 'axum' features). When provided, uses --no-default-features --features=a,b,c. When omitted, uses --all-features with automatic fallback."
)]
pub features: Option<Vec<String>>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
Expand All @@ -81,6 +85,10 @@ pub struct CacheCrateFromCratesIOParams {
description = "Force re-download and re-cache the crate even if it already exists. Defaults to false. The existing cache is preserved until the update succeeds."
)]
pub update: Option<bool>,
#[schemars(
description = "Specific features to enable instead of --all-features. When provided, uses --no-default-features --features=a,b,c."
)]
pub features: Option<Vec<String>>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
Expand All @@ -105,6 +113,10 @@ pub struct CacheCrateFromGitHubParams {
description = "Force re-download and re-cache the crate even if it already exists. Defaults to false. The existing cache is preserved until the update succeeds."
)]
pub update: Option<bool>,
#[schemars(
description = "Specific features to enable instead of --all-features. When provided, uses --no-default-features --features=a,b,c."
)]
pub features: Option<Vec<String>>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
Expand All @@ -127,6 +139,10 @@ pub struct CacheCrateFromLocalParams {
description = "Force re-download and re-cache the crate even if it already exists. Defaults to false. The existing cache is preserved until the update succeeds."
)]
pub update: Option<bool>,
#[schemars(
description = "Specific features to enable instead of --all-features. When provided, uses --no-default-features --features=a,b,c."
)]
pub features: Option<Vec<String>>,
}

#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
Expand Down Expand Up @@ -766,6 +782,7 @@ impl CacheTools {
version: params.version.clone().unwrap(),
members: params.members.clone(),
update: params.update,
features: params.features.clone(),
}),
"github" => CrateSource::GitHub(CacheCrateFromGitHubParams {
crate_name: params.crate_name.clone(),
Expand All @@ -774,13 +791,15 @@ impl CacheTools {
tag: params.tag.clone(),
members: params.members.clone(),
update: params.update,
features: params.features.clone(),
}),
"local" => CrateSource::LocalPath(CacheCrateFromLocalParams {
crate_name: params.crate_name.clone(),
version: params.version.clone(),
path: params.path.clone().unwrap(),
members: params.members.clone(),
update: params.update,
features: params.features.clone(),
}),
_ => unreachable!("Invalid source type should have been caught earlier"),
}
Expand Down
Loading
Loading