Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,22 @@ cargo build --release
### CLI Commands

```bash
rust-docs-mcp # Start MCP server
rust-docs-mcp # Start MCP server (same as `serve`)
rust-docs-mcp serve # Start MCP server explicitly

# One-shot operations (stateless – print JSON to stdout and exit)
rust-docs-mcp call cache-crate \
--params '{"crate_name":"serde","source_type":"cratesio","version":"1.0.215"}'

rust-docs-mcp call search-items-fuzzy \
--params '{"crate_name":"serde","version":"1.0.215","query":"Deserialize","limit":10}'

rust-docs-mcp call list-cached-crates

rust-docs-mcp call list-crate-versions \
--params '{"crate_name":"serde"}'

# Maintenance commands
rust-docs-mcp install # Install to ~/.local/bin
rust-docs-mcp install --force # Force overwrite existing installation
rust-docs-mcp doctor # Verify system environment and dependencies
Expand All @@ -227,6 +242,19 @@ rust-docs-mcp update # Update to latest version from GitHub
rust-docs-mcp --help # Show help
```

> **Note:** `call cache-crate` is **blocking** — it downloads the crate,
> generates documentation, and builds the search index before returning.
> This is different from MCP mode where `cache_crate` returns a task ID
> immediately and completes in the background. If a one-shot tool returns
> an error JSON response, the CLI prints that JSON to stdout and exits with
> status 1.
>
> Available one-shot tools: `cache-crate`, `search-items-fuzzy`,
> `search-items-preview`, `search-items`, `list-crate-items`,
> `get-item-details`, `get-item-docs`, `get-item-source`,
> `list-cached-crates`, `list-crate-versions`, `get-dependencies`,
> `structure`.

### Troubleshooting

If you encounter issues during installation or runtime, run the doctor command
Expand Down
238 changes: 137 additions & 101 deletions rust-docs-mcp/src/cache/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,69 @@ pub struct CacheTools {
task_manager: Arc<TaskManager>,
}

/// Convert [`CacheCrateParams`] into a [`CrateSource`], returning a
/// user-facing error string when validation fails.
pub fn params_to_source_checked(params: &CacheCrateParams) -> Result<CrateSource, String> {
match params.source_type.as_str() {
"cratesio" => {
let version = params.version.clone().ok_or_else(|| {
"Missing required parameter 'version' for source_type='cratesio'".to_string()
})?;
Ok(CrateSource::CratesIO(CacheCrateFromCratesIOParams {
crate_name: params.crate_name.clone(),
version,
members: params.members.clone(),
update: params.update,
}))
}
"github" => {
let github_url = params.github_url.clone().ok_or_else(|| {
"Missing required parameter 'github_url' for source_type='github'".to_string()
})?;

match (&params.branch, &params.tag) {
(Some(_), Some(_)) => {
return Err(
"Only one of 'branch' or 'tag' can be specified for source_type='github', not both"
.to_string(),
);
}
(None, None) => {
return Err(
"Either 'branch' or 'tag' must be specified for source_type='github'"
.to_string(),
);
}
_ => {}
}

Ok(CrateSource::GitHub(CacheCrateFromGitHubParams {
crate_name: params.crate_name.clone(),
github_url,
branch: params.branch.clone(),
tag: params.tag.clone(),
members: params.members.clone(),
update: params.update,
}))
}
"local" => {
let path = params.path.clone().ok_or_else(|| {
"Missing required parameter 'path' for source_type='local'".to_string()
})?;
Ok(CrateSource::LocalPath(CacheCrateFromLocalParams {
crate_name: params.crate_name.clone(),
version: params.version.clone(),
path,
members: params.members.clone(),
update: params.update,
}))
}
other => Err(format!(
"Invalid source_type '{other}'. Must be one of: 'cratesio', 'github', 'local'"
)),
}
}

impl CacheTools {
/// Create a new CacheTools instance
pub fn new(cache: Arc<RwLock<CrateCache>>, task_manager: Arc<TaskManager>) -> Self {
Expand Down Expand Up @@ -571,96 +634,88 @@ impl CacheTools {
}
}

/// Unified cache_crate method that accepts all source types
/// Build task metadata from an already-validated source.
///
/// Validates parameters, spawns async task, and returns immediately with task ID.
/// Returns JSON-formatted [`CacheTaskStartedOutput`] for structured monitoring.
pub async fn cache_crate(&self, params: CacheCrateParams) -> String {
// Validate and extract source details for task creation
let (crate_name, version, source_details) = match params.source_type.as_str() {
"cratesio" => {
let version = match &params.version {
Some(v) => v.clone(),
None => {
return "# Error\n\nMissing required parameter 'version' for source_type='cratesio'".to_string();
}
};
(params.crate_name.clone(), version, None)
}
"github" => {
let github_url = match &params.github_url {
Some(url) => url.clone(),
None => {
return "# Error\n\nMissing required parameter 'github_url' for source_type='github'".to_string();
}
};

match (&params.branch, &params.tag) {
/// For local paths, this also resolves and stores the effective version so
/// the spawned background task does not need to rebuild the source from raw
/// params.
fn task_metadata_from_source(
source: &mut CrateSource,
) -> Result<(String, String, String, Option<String>), String> {
match source {
CrateSource::CratesIO(params) => Ok((
params.crate_name.clone(),
params.version.clone(),
"cratesio".to_string(),
None,
)),
CrateSource::GitHub(params) => {
let (version, ref_type) = match (&params.branch, &params.tag) {
(Some(branch), None) => (branch.clone(), "branch"),
(None, Some(tag)) => (tag.clone(), "tag"),
(Some(_), Some(_)) => {
return "# Error\n\nOnly one of 'branch' or 'tag' can be specified for source_type='github', not both".to_string();
return Err(
"Only one of 'branch' or 'tag' can be specified for source_type='github', not both"
.to_string(),
);
}
(None, None) => {
return "# Error\n\nEither 'branch' or 'tag' must be specified for source_type='github'".to_string();
}
_ => {}
}

let version = params
.branch
.clone()
.or_else(|| params.tag.clone())
.unwrap();
let ref_type = if params.branch.is_some() {
"branch"
} else {
"tag"
};
let details = format!("{github_url}, {ref_type}: {version}");
(params.crate_name.clone(), version, Some(details))
}
"local" => {
let path = match &params.path {
Some(p) => p.clone(),
None => {
return "# Error\n\nMissing required parameter 'path' for source_type='local'".to_string();
return Err(
"Either 'branch' or 'tag' must be specified for source_type='github'"
.to_string(),
);
}
};

// Resolve version synchronously before creating task (fixes bug #2)
Ok((
params.crate_name.clone(),
version.clone(),
"github".to_string(),
Some(format!("{}, {ref_type}: {version}", params.github_url)),
))
}
CrateSource::LocalPath(params) => {
let (version, auto_detected) =
match Self::resolve_local_version(&path, params.version.as_deref()) {
Ok(result) => result,
Err(error_msg) => {
return format!("# Error\n\n{error_msg}");
}
};
Self::resolve_local_version(&params.path, params.version.as_deref())?;
params.version = Some(version.clone());

// Add auto-detection note to source details
let details = if auto_detected {
format!("{path} (version auto-detected from Cargo.toml)")
format!("{} (version auto-detected from Cargo.toml)", params.path)
} else {
path
params.path.clone()
};

(params.crate_name.clone(), version, Some(details))
}
_ => {
return format!(
"# Error\n\nInvalid source_type '{}'. Must be one of: 'cratesio', 'github', 'local'",
params.source_type
);
Ok((
params.crate_name.clone(),
version,
"local".to_string(),
Some(details),
))
}
}
}

/// Unified cache_crate method that accepts all source types
///
/// Validates parameters, spawns async task, and returns immediately with task ID.
/// Returns JSON-formatted [`CacheTaskStartedOutput`] for structured monitoring.
pub async fn cache_crate(&self, params: CacheCrateParams) -> String {
let mut crate_source = match params_to_source_checked(&params) {
Ok(source) => source,
Err(error) => return format!("# Error\n\n{error}"),
};

// Validate and extract source details for task creation.
let (crate_name, version, source_type, source_details) =
match Self::task_metadata_from_source(&mut crate_source) {
Ok(metadata) => metadata,
Err(error) => return format!("# Error\n\n{error}"),
};

// Create task
let task = self
.task_manager
.create_task(
crate_name,
version,
params.source_type.clone(),
source_details,
)
.create_task(crate_name, version, source_type, source_details)
.await;

// Update status to InProgress before returning (fixes race condition bug #1)
Expand All @@ -673,12 +728,8 @@ impl CacheTools {
let task_manager = self.task_manager.clone();
let task_id = task.task_id.clone();
let cancellation_token = task.cancellation_token.clone();
let params = params.clone(); // Clone params for the spawned task

tokio::spawn(async move {
// Build CrateSource from params
let crate_source = Self::params_to_source(&params);

// Run the caching operation
let cache_guard = cache.write().await;

Expand Down Expand Up @@ -758,32 +809,17 @@ impl CacheTools {
output.to_json()
}

/// Helper to convert CacheCrateParams to CrateSource
fn params_to_source(params: &CacheCrateParams) -> CrateSource {
match params.source_type.as_str() {
"cratesio" => CrateSource::CratesIO(CacheCrateFromCratesIOParams {
crate_name: params.crate_name.clone(),
version: params.version.clone().unwrap(),
members: params.members.clone(),
update: params.update,
}),
"github" => CrateSource::GitHub(CacheCrateFromGitHubParams {
crate_name: params.crate_name.clone(),
github_url: params.github_url.clone().unwrap(),
branch: params.branch.clone(),
tag: params.tag.clone(),
members: params.members.clone(),
update: params.update,
}),
"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,
}),
_ => unreachable!("Invalid source type should have been caught earlier"),
}
/// Blocking (one-shot) cache: validate params, run the full pipeline,
/// and return the result JSON. The call does **not** return until
/// download, doc generation, and search-index creation finish.
pub async fn cache_crate_blocking(&self, params: CacheCrateParams) -> String {
let source = match params_to_source_checked(&params) {
Ok(source) => source,
Err(error) => return CacheCrateOutput::Error { error }.to_json(),
};

let cache = self.cache.write().await;
cache.cache_crate_with_source(source, None, None).await
}

/// Unified cache_operations method for managing and monitoring caching tasks
Expand Down
Loading
Loading