Skip to main content

crates_docs/tools/docs/
lookup_item.rs

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