Skip to main content

crates_docs/tools/docs/
lookup_crate.rs

1//! Lookup crate documentation tool
2#![allow(missing_docs)]
3
4use crate::tools::docs::html;
5use crate::tools::docs::DocService;
6use crate::tools::Tool;
7use async_trait::async_trait;
8use rust_mcp_sdk::schema::CallToolError;
9use serde::{Deserialize, Serialize};
10use std::sync::Arc;
11
12const TOOL_NAME: &str = "lookup_crate";
13
14/// Lookup crate documentation tool
15#[rust_mcp_sdk::macros::mcp_tool(
16    name = "lookup_crate",
17    title = "Lookup Crate Documentation",
18    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.",
19    destructive_hint = false,
20    idempotent_hint = true,
21    open_world_hint = false,
22    read_only_hint = true,
23    execution(task_support = "optional"),
24    icons = [
25        (src = "https://docs.rs/favicon.ico", mime_type = "image/x-icon", sizes = ["32x32"], theme = "light"),
26        (src = "https://docs.rs/favicon.ico", mime_type = "image/x-icon", sizes = ["32x32"], theme = "dark")
27    ]
28)]
29#[allow(missing_docs)]
30#[derive(Debug, Clone, Deserialize, Serialize, rust_mcp_sdk::macros::JsonSchema)]
31pub struct LookupCrateTool {
32    /// Crate name
33    #[json_schema(
34        title = "Crate Name",
35        description = "Crate name to lookup, e.g.: serde, tokio, reqwest"
36    )]
37    pub crate_name: String,
38
39    /// Version (optional, defaults to latest)
40    #[json_schema(
41        title = "Version",
42        description = "Crate version, e.g.: 1.0.0. Uses latest version if not specified"
43    )]
44    pub version: Option<String>,
45
46    /// Output format: markdown, text, or html
47    #[json_schema(
48        title = "Output Format",
49        description = "Output format: markdown (default), text (plain text), html",
50        default = "markdown"
51    )]
52    pub format: Option<String>,
53}
54
55/// Lookup crate documentation tool implementation
56pub struct LookupCrateToolImpl {
57    service: Arc<DocService>,
58}
59
60impl LookupCrateToolImpl {
61    /// Create a new lookup tool instance
62    #[must_use]
63    pub fn new(service: Arc<DocService>) -> Self {
64        Self { service }
65    }
66
67    /// Build docs.rs URL for crate
68    fn build_url(crate_name: &str, version: Option<&str>) -> String {
69        super::build_docs_url(crate_name, version)
70    }
71
72    /// Get crate documentation (markdown format)
73    async fn fetch_crate_docs(
74        &self,
75        crate_name: &str,
76        version: Option<&str>,
77    ) -> std::result::Result<String, CallToolError> {
78        // Try cache first
79        if let Some(cached) = self
80            .service
81            .doc_cache()
82            .get_crate_docs(crate_name, version)
83            .await
84        {
85            return Ok(cached);
86        }
87
88        // Fetch from docs.rs
89        let url = Self::build_url(crate_name, version);
90        let html = self.service.fetch_html(&url, Some(TOOL_NAME)).await?;
91
92        // Extract documentation
93        let docs = html::extract_documentation(&html);
94
95        // Cache result
96        self.service
97            .doc_cache()
98            .set_crate_docs(crate_name, version, docs.clone())
99            .await
100            .map_err(|e| {
101                CallToolError::from_message(format!("[{TOOL_NAME}] Cache set failed: {e}"))
102            })?;
103
104        Ok(docs)
105    }
106
107    /// Get crate documentation as plain text
108    async fn fetch_crate_docs_as_text(
109        &self,
110        crate_name: &str,
111        version: Option<&str>,
112    ) -> std::result::Result<String, CallToolError> {
113        let url = Self::build_url(crate_name, version);
114        let html = self.service.fetch_html(&url, Some(TOOL_NAME)).await?;
115        Ok(html::html_to_text(&html))
116    }
117
118    /// Get crate documentation as raw HTML
119    async fn fetch_crate_docs_as_html(
120        &self,
121        crate_name: &str,
122        version: Option<&str>,
123    ) -> std::result::Result<String, CallToolError> {
124        let url = Self::build_url(crate_name, version);
125        self.service.fetch_html(&url, Some(TOOL_NAME)).await
126    }
127}
128
129#[async_trait]
130impl Tool for LookupCrateToolImpl {
131    fn definition(&self) -> rust_mcp_sdk::schema::Tool {
132        LookupCrateTool::tool()
133    }
134
135    async fn execute(
136        &self,
137        arguments: serde_json::Value,
138    ) -> std::result::Result<
139        rust_mcp_sdk::schema::CallToolResult,
140        rust_mcp_sdk::schema::CallToolError,
141    > {
142        let params: LookupCrateTool = serde_json::from_value(arguments).map_err(|e| {
143            rust_mcp_sdk::schema::CallToolError::invalid_arguments(
144                "lookup_crate",
145                Some(format!("Parameter parsing failed: {e}")),
146            )
147        })?;
148
149        let format = super::parse_format(params.format.as_deref()).map_err(|_| {
150            rust_mcp_sdk::schema::CallToolError::invalid_arguments(
151                "lookup_crate",
152                Some("Invalid format".to_string()),
153            )
154        })?;
155        let content = match format {
156            super::Format::Text => {
157                self.fetch_crate_docs_as_text(&params.crate_name, params.version.as_deref())
158                    .await?
159            }
160            super::Format::Html => {
161                self.fetch_crate_docs_as_html(&params.crate_name, params.version.as_deref())
162                    .await?
163            }
164            super::Format::Json => {
165                return Err(rust_mcp_sdk::schema::CallToolError::invalid_arguments(
166                    "lookup_crate",
167                    Some("JSON format is not supported by this tool".to_string()),
168                ))
169            }
170            super::Format::Markdown => {
171                self.fetch_crate_docs(&params.crate_name, params.version.as_deref())
172                    .await?
173            }
174        };
175
176        Ok(rust_mcp_sdk::schema::CallToolResult::text_content(vec![
177            content.into(),
178        ]))
179    }
180}
181
182impl Default for LookupCrateToolImpl {
183    fn default() -> Self {
184        Self::new(Arc::new(super::DocService::default()))
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use serial_test::serial;
192
193    #[test]
194    #[serial]
195    fn test_build_url_without_version() {
196        std::env::set_var("CRATES_DOCS_DOCS_RS_URL", "https://docs.rs");
197        let url = LookupCrateToolImpl::build_url("serde", None);
198        assert_eq!(url, "https://docs.rs/serde/");
199        std::env::remove_var("CRATES_DOCS_DOCS_RS_URL");
200    }
201
202    #[test]
203    #[serial]
204    fn test_build_url_with_version() {
205        std::env::set_var("CRATES_DOCS_DOCS_RS_URL", "https://docs.rs");
206        let url = LookupCrateToolImpl::build_url("serde", Some("1.0.0"));
207        assert_eq!(url, "https://docs.rs/serde/1.0.0/");
208        std::env::remove_var("CRATES_DOCS_DOCS_RS_URL");
209    }
210
211    #[test]
212    #[serial]
213    fn test_build_url_with_custom_base() {
214        std::env::set_var("CRATES_DOCS_DOCS_RS_URL", "http://mock-server");
215        let url = LookupCrateToolImpl::build_url("serde", None);
216        assert_eq!(url, "http://mock-server/serde/");
217        std::env::remove_var("CRATES_DOCS_DOCS_RS_URL");
218    }
219}