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