rustdoc-mcp 0.6.2

mcp server for rustdocs
use crate::request::Request;
use crate::{state::RustdocTools, traits::WriteFmt};
use anyhow::Result;
use clap::Args;
use mcplease::traits::{Tool, WithExamples};
use mcplease::types::Example;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Default, Debug, Serialize, Deserialize, Args, JsonSchema)]
#[serde(rename = "list_crates")]
/// List available crates in the workspace, including dependencies
pub struct ListCrates {
    /// Optional workspace member to scope dependencies to
    #[arg(long)]
    pub workspace_member: Option<String>,
    #[serde(skip)]
    pub for_schemars: (),
}

impl WithExamples for ListCrates {
    fn examples() -> Vec<Example<Self>> {
        vec![Example {
            description: "listing crates",
            item: Self::default(),
        }]
    }
}

impl Tool<RustdocTools> for ListCrates {
    fn execute(self, state: &mut RustdocTools) -> Result<String> {
        let request = Request::new(state.working_directory(None)?);

        let mut result = String::new();
        let root_crate = request.local_source().and_then(|ls| ls.root_crate());

        let mut available_crates = request
            .list_available_crates()
            .filter(|c| {
                root_crate.is_none_or(|rc| {
                    !c.provenance().is_local_dependency() || c.used_by().iter().any(|u| **u == **rc)
                })
            })
            .collect::<Vec<_>>();

        available_crates.sort_by(|a, b| a.name().cmp(b.name()));

        for crate_info in available_crates {
            let crate_name = crate_info.name();

            let note = if crate_info.is_default_crate() {
                " (workspace-local, aliased as \"crate\")".to_string()
            } else if crate_info.provenance().is_workspace() {
                " (workspace-local)".to_string()
            } else if let Some(version) = crate_info.version() {
                // Add workspace member usage info when showing full workspace view
                let usage_info = if !crate_info.used_by().is_empty() {
                    format!(" ({})", crate_info.used_by().join(", "))
                } else {
                    String::new()
                };

                format!(" {version}{usage_info}")
            } else {
                String::new()
            };
            result.write_fmt(format_args!("• {crate_name}{note}\n"));
            if let Some(description) = crate_info.description() {
                let description = description.replace('\n', " ");
                result.write_fmt(format_args!("    {description}\n"));
            }
        }

        Ok(result)
    }
}