rust-docs-mcp 0.1.1

MCP server providing comprehensive Rust crate analysis: documentation search, source code access, dependency trees, and module structure visualization with multi-source caching
Documentation
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::RwLock;

use rmcp::schemars;
use serde::{Deserialize, Serialize};

use crate::analysis::outputs::{AnalysisErrorOutput, StructureNode, StructureOutput};
use crate::cache::{CrateCache, workspace::WorkspaceHandler};

// Use StructureNode from outputs module instead

#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
pub struct AnalyzeCrateStructureParams {
    #[schemars(description = "The name of the crate")]
    pub crate_name: String,

    #[schemars(description = "The version of the crate")]
    pub version: String,

    #[schemars(
        description = "For workspace crates, specify the member path (e.g., 'crates/rmcp')"
    )]
    pub member: Option<String>,

    #[schemars(description = "Process only this package's library")]
    pub lib: Option<bool>,

    #[schemars(description = "Process only the specified binary")]
    pub bin: Option<String>,

    #[schemars(description = "Do not activate the default feature")]
    pub no_default_features: Option<bool>,

    #[schemars(description = "Activate all available features")]
    pub all_features: Option<bool>,

    #[schemars(
        description = "List of features to activate. This will be ignored if all_features is provided"
    )]
    pub features: Option<Vec<String>>,

    #[schemars(description = "Analyze for target triple")]
    pub target: Option<String>,

    #[schemars(description = "Analyze with cfg(test) enabled (i.e as if built via cargo test)")]
    pub cfg_test: Option<bool>,

    #[schemars(description = "Filter out functions (e.g. fns, async fns, const fns) from tree")]
    pub no_fns: Option<bool>,

    #[schemars(description = "Filter out traits (e.g. trait, unsafe trait) from tree")]
    pub no_traits: Option<bool>,

    #[schemars(description = "Filter out types (e.g. structs, unions, enums) from tree")]
    pub no_types: Option<bool>,

    #[schemars(description = "The sorting order to use (e.g. name, visibility, kind)")]
    pub sort_by: Option<String>,

    #[schemars(description = "Reverses the sorting order")]
    pub sort_reversed: Option<bool>,

    #[schemars(description = "Focus the graph on a particular path or use-tree's environment")]
    pub focus_on: Option<String>,

    #[schemars(
        description = "The maximum depth of the generated graph relative to the crate's root node, or nodes selected by 'focus_on'"
    )]
    pub max_depth: Option<i64>,
}

#[derive(Debug, Clone)]
pub struct AnalysisTools {
    cache: Arc<RwLock<CrateCache>>,
}

impl AnalysisTools {
    pub fn new(cache: Arc<RwLock<CrateCache>>) -> Self {
        Self { cache }
    }

    pub async fn structure(
        &self,
        params: AnalyzeCrateStructureParams,
    ) -> Result<StructureOutput, AnalysisErrorOutput> {
        let cache = self.cache.write().await;

        // Ensure the crate source is available (without requiring docs)
        match cache
            .ensure_crate_or_member_source(
                &params.crate_name,
                &params.version,
                params.member.as_deref(),
                None, // Use default source
            )
            .await
        {
            Ok(source_path) => {
                // The source_path already points to the correct location
                // (either the crate root or the member directory)
                let manifest_path = source_path.join("Cargo.toml");

                // Get the actual package name from Cargo.toml for workspace members
                let package = if params.member.is_some() {
                    WorkspaceHandler::get_package_name(&manifest_path).ok()
                } else {
                    None
                };

                drop(cache); // Release the lock before the blocking operation

                // Run the analysis
                analyze_with_cargo_modules(manifest_path, package, params).await
            }
            Err(e) => Err(AnalysisErrorOutput::new(format!(
                "Failed to ensure crate source is available: {e}"
            ))),
        }
    }
}

async fn analyze_with_cargo_modules(
    manifest_path: PathBuf,
    package: Option<String>,
    params: AnalyzeCrateStructureParams,
) -> Result<StructureOutput, AnalysisErrorOutput> {
    // Run the analysis synchronously in a blocking task
    let result = tokio::task::spawn_blocking(move || -> Result<StructureOutput, String> {
        // Configure analysis settings
        let config = rust_analyzer_modules::AnalysisConfig {
            cfg_test: params.cfg_test.unwrap_or(false),
            sysroot: false,
            no_default_features: params.no_default_features.unwrap_or(false),
            all_features: params.all_features.unwrap_or(false),
            features: params.features.unwrap_or_default(),
        };

        // Analyze the crate using the public API
        let (crate_id, analysis_host, edition) = rust_analyzer_modules::analyze_crate(
            &manifest_path.parent().unwrap(),
            package.as_deref(),
            config,
        )
        .map_err(|e| format!("Failed to analyze crate: {e}"))?;

        let db = analysis_host.raw_database();

        // Build the tree using the public API
        let builder = rust_analyzer_modules::TreeBuilder::new(db, crate_id);
        let tree = builder
            .build()
            .map_err(|e| format!("Failed to build tree: {e}"))?;

        // Format the tree structure
        let tree_node = format_tree(&tree, db, edition);
        Ok(StructureOutput {
            status: "success".to_string(),
            message: "Module structure analysis completed".to_string(),
            tree: tree_node,
            usage_hint: "Use the 'path' and 'name' fields to search for items with search_items_preview tool".to_string(),
        })
    })
    .await;

    match result {
        Ok(Ok(output)) => Ok(output),
        Ok(Err(e)) => Err(AnalysisErrorOutput::new(format!("Analysis failed: {e}"))),
        Err(e) => Err(AnalysisErrorOutput::new(format!("Task failed: {e}"))),
    }
}

/// Helper function to format the tree structure with enhanced information
fn format_tree(
    tree: &rust_analyzer_modules::Tree<rust_analyzer_modules::Item>,
    db: &ra_ap_ide::RootDatabase,
    edition: ra_ap_ide::Edition,
) -> StructureNode {
    fn format_node(
        node: &rust_analyzer_modules::Tree<rust_analyzer_modules::Item>,
        db: &ra_ap_ide::RootDatabase,
        edition: ra_ap_ide::Edition,
    ) -> StructureNode {
        let item = &node.node;

        // Extract readable information
        let kind = item.kind_display_name(db, edition).to_string();
        let name = item.display_name(db, edition);
        let path = item.display_path(db, edition);
        let visibility = item.visibility(db, edition).to_string();

        StructureNode {
            kind,
            name,
            path,
            visibility,
            children: if node.subtrees.is_empty() {
                None
            } else {
                Some(
                    node.subtrees
                        .iter()
                        .map(|subtree| format_node(subtree, db, edition))
                        .collect(),
                )
            },
        }
    }

    format_node(tree, db, edition)
}