Skip to main content

crates_docs/tools/docs/
lookup_crate.rs

1//! Lookup crate documentation tool
2//!
3//! Provides functionality to retrieve complete documentation for a Rust crate
4//! from docs.rs. Returns the main documentation page content including modules,
5//! structs, functions, etc.
6
7#![allow(missing_docs)]
8
9//! Tool parameters for looking up crate documentation from docs.rs
10//!
11//! This struct defines the parameters needed to retrieve documentation
12//! for a specific Rust crate, including the crate name, optional version,
13//! and desired output format.
14
15use crate::tools::docs::html;
16use crate::tools::docs::DocService;
17use crate::tools::Tool;
18use async_trait::async_trait;
19use rust_mcp_sdk::schema::CallToolError;
20use serde::{Deserialize, Serialize};
21use std::sync::Arc;
22
23const TOOL_NAME: &str = "lookup_crate";
24///
25/// Used to specify which crate to look up and in what format to return the documentation.
26#[rust_mcp_sdk::macros::mcp_tool(
27    name = "lookup_crate",
28    title = "Lookup Crate Documentation",
29    description = "Get complete documentation for a Rust crate from docs.rs. Returns the main documentation page content, including modules, structs, functions, etc. Suitable for understanding the overall functionality and usage of a crate.",
30    destructive_hint = false,
31    idempotent_hint = true,
32    open_world_hint = false,
33    read_only_hint = true,
34    execution(task_support = "optional"),
35    icons = [
36        (src = "https://docs.rs/favicon.ico", mime_type = "image/x-icon", sizes = ["32x32"], theme = "light"),
37        (src = "https://docs.rs/favicon.ico", mime_type = "image/x-icon", sizes = ["32x32"], theme = "dark")
38    ]
39)]
40/// Parameters for the `lookup_crate` tool
41///
42/// Defines the input parameters for retrieving crate documentation,
43/// including the crate name, optional version specification, and output format.
44#[derive(Debug, Clone, Deserialize, Serialize, rust_mcp_sdk::macros::JsonSchema)]
45pub struct LookupCrateTool {
46    /// Crate name to lookup (e.g., "serde", "tokio", "reqwest")
47    #[json_schema(
48        title = "Crate Name",
49        description = "Crate name to lookup, e.g.: serde, tokio, reqwest"
50    )]
51    pub crate_name: String,
52
53    /// Crate version (optional, defaults to latest)
54    #[json_schema(
55        title = "Version",
56        description = "Crate version, e.g.: 1.0.0. Uses latest version if not specified"
57    )]
58    pub version: Option<String>,
59
60    /// Output format: "markdown", "text", or "html" (defaults to "markdown")
61    #[json_schema(
62        title = "Output Format",
63        description = "Output format: markdown (default), text (plain text), html",
64        default = "markdown"
65    )]
66    pub format: Option<String>,
67}
68
69/// Implementation of the lookup crate documentation tool
70///
71/// Handles the execution of crate documentation lookups, including
72/// cache management, HTTP fetching from docs.rs, and result formatting.
73pub struct LookupCrateToolImpl {
74    /// Shared document service for HTTP requests and caching
75    service: Arc<DocService>,
76}
77
78impl LookupCrateToolImpl {
79    /// Create a new lookup tool instance
80    #[must_use]
81    pub fn new(service: Arc<DocService>) -> Self {
82        Self { service }
83    }
84
85    /// Build docs.rs URL for crate
86    fn build_url(crate_name: &str, version: Option<&str>) -> String {
87        super::build_docs_url(crate_name, version)
88    }
89
90    async fn fetch_crate_html(
91        &self,
92        crate_name: &str,
93        version: Option<&str>,
94    ) -> std::result::Result<String, CallToolError> {
95        if let Some(cached) = self
96            .service
97            .doc_cache()
98            .get_crate_html(crate_name, version)
99            .await
100        {
101            return Ok(cached.to_string());
102        }
103
104        let url = Self::build_url(crate_name, version);
105        let html = self.service.fetch_html(&url, Some(TOOL_NAME)).await?;
106
107        self.service
108            .doc_cache()
109            .set_crate_html(crate_name, version, html.clone())
110            .await
111            .map_err(|e| {
112                CallToolError::from_message(format!("[{TOOL_NAME}] Cache set failed: {e}"))
113            })?;
114
115        Ok(html)
116    }
117
118    /// Get crate documentation (markdown format)
119    ///
120    /// Returns `Arc<str>` to preserve shared ownership on cache hits,
121    /// avoiding unnecessary cloning of large documentation strings.
122    async fn fetch_crate_docs(
123        &self,
124        crate_name: &str,
125        version: Option<&str>,
126    ) -> std::result::Result<Arc<str>, CallToolError> {
127        // Try cache first - returns Arc<str> directly without cloning
128        if let Some(cached) = self
129            .service
130            .doc_cache()
131            .get_crate_docs(crate_name, version)
132            .await
133        {
134            return Ok(cached);
135        }
136
137        let html = self.fetch_crate_html(crate_name, version).await?;
138
139        // Extract documentation into Arc<str> for shared ownership
140        let docs: Arc<str> = Arc::from(html::extract_documentation(&html).into_boxed_str());
141
142        // Cache result - convert Arc<str> to String for the cache
143        self.service
144            .doc_cache()
145            .set_crate_docs(crate_name, version, docs.to_string())
146            .await
147            .map_err(|e| {
148                CallToolError::from_message(format!("[{TOOL_NAME}] Cache set failed: {e}"))
149            })?;
150
151        Ok(docs)
152    }
153
154    /// Get crate documentation as plain text
155    async fn fetch_crate_docs_as_text(
156        &self,
157        crate_name: &str,
158        version: Option<&str>,
159    ) -> std::result::Result<String, CallToolError> {
160        let html = self.fetch_crate_html(crate_name, version).await?;
161        Ok(html::html_to_text(&html))
162    }
163
164    /// Get crate documentation as raw HTML
165    async fn fetch_crate_docs_as_html(
166        &self,
167        crate_name: &str,
168        version: Option<&str>,
169    ) -> std::result::Result<String, CallToolError> {
170        self.fetch_crate_html(crate_name, version).await
171    }
172}
173
174#[async_trait]
175impl Tool for LookupCrateToolImpl {
176    fn definition(&self) -> rust_mcp_sdk::schema::Tool {
177        LookupCrateTool::tool()
178    }
179
180    async fn execute(
181        &self,
182        arguments: serde_json::Value,
183    ) -> std::result::Result<
184        rust_mcp_sdk::schema::CallToolResult,
185        rust_mcp_sdk::schema::CallToolError,
186    > {
187        let params: LookupCrateTool = serde_json::from_value(arguments).map_err(|e| {
188            rust_mcp_sdk::schema::CallToolError::invalid_arguments(
189                "lookup_crate",
190                Some(format!("Parameter parsing failed: {e}")),
191            )
192        })?;
193
194        let format = super::parse_format(params.format.as_deref()).map_err(|_| {
195            rust_mcp_sdk::schema::CallToolError::invalid_arguments(
196                "lookup_crate",
197                Some("Invalid format".to_string()),
198            )
199        })?;
200        let content = match format {
201            super::Format::Text => {
202                self.fetch_crate_docs_as_text(&params.crate_name, params.version.as_deref())
203                    .await?
204            }
205            super::Format::Html => {
206                self.fetch_crate_docs_as_html(&params.crate_name, params.version.as_deref())
207                    .await?
208            }
209            super::Format::Json => {
210                return Err(rust_mcp_sdk::schema::CallToolError::invalid_arguments(
211                    "lookup_crate",
212                    Some("JSON format is not supported by this tool".to_string()),
213                ))
214            }
215            super::Format::Markdown => self
216                .fetch_crate_docs(&params.crate_name, params.version.as_deref())
217                .await
218                .map(|arc| arc.to_string())?,
219        };
220
221        Ok(rust_mcp_sdk::schema::CallToolResult::text_content(vec![
222            content.into(),
223        ]))
224    }
225}
226
227impl Default for LookupCrateToolImpl {
228    fn default() -> Self {
229        Self::new(Arc::new(super::DocService::default()))
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236    use serial_test::serial;
237
238    #[test]
239    #[serial]
240    fn test_build_url_without_version() {
241        std::env::set_var("CRATES_DOCS_DOCS_RS_URL", "https://docs.rs");
242        let url = LookupCrateToolImpl::build_url("serde", None);
243        assert_eq!(url, "https://docs.rs/serde/");
244        std::env::remove_var("CRATES_DOCS_DOCS_RS_URL");
245    }
246
247    #[test]
248    #[serial]
249    fn test_build_url_with_version() {
250        std::env::set_var("CRATES_DOCS_DOCS_RS_URL", "https://docs.rs");
251        let url = LookupCrateToolImpl::build_url("serde", Some("1.0.0"));
252        assert_eq!(url, "https://docs.rs/serde/1.0.0/");
253        std::env::remove_var("CRATES_DOCS_DOCS_RS_URL");
254    }
255
256    #[test]
257    #[serial]
258    fn test_build_url_with_custom_base() {
259        std::env::set_var("CRATES_DOCS_DOCS_RS_URL", "http://mock-server");
260        let url = LookupCrateToolImpl::build_url("serde", None);
261        assert_eq!(url, "http://mock-server/serde/");
262        std::env::remove_var("CRATES_DOCS_DOCS_RS_URL");
263    }
264}