Skip to main content

crates_docs/tools/docs/
lookup_item.rs

1//! Lookup item 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_item";
13
14/// Lookup item documentation tool
15#[rust_mcp_sdk::macros::mcp_tool(
16    name = "lookup_item",
17    title = "Lookup Item Documentation",
18    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.",
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 LookupItemTool {
32    /// Crate name
33    #[json_schema(
34        title = "Crate Name",
35        description = "Crate name to lookup, e.g.: serde, tokio, std"
36    )]
37    pub crate_name: String,
38
39    /// Item path (e.g., `std::collections::HashMap`)
40    #[json_schema(
41        title = "Item Path",
42        description = "Item path in format 'module::submodule::item', e.g.: serde::Serialize, tokio::runtime::Runtime, std::collections::HashMap"
43    )]
44    pub item_path: String,
45
46    /// Version (optional, defaults to latest)
47    #[json_schema(
48        title = "Version",
49        description = "Crate version. Uses latest version if not specified"
50    )]
51    pub version: Option<String>,
52
53    /// Output format: markdown, text, or html
54    #[json_schema(
55        title = "Output Format",
56        description = "Output format: markdown (default), text (plain text), html",
57        default = "markdown"
58    )]
59    pub format: Option<String>,
60}
61
62/// Lookup item documentation tool implementation
63pub struct LookupItemToolImpl {
64    service: Arc<DocService>,
65}
66
67impl LookupItemToolImpl {
68    /// Create a new lookup item tool instance
69    #[must_use]
70    pub fn new(service: Arc<DocService>) -> Self {
71        Self { service }
72    }
73
74    /// Build docs.rs search URL for item
75    fn build_search_url(crate_name: &str, item_path: &str, version: Option<&str>) -> String {
76        super::build_docs_item_url(crate_name, version, item_path)
77    }
78
79    /// Get item documentation (markdown format)
80    async fn fetch_item_docs(
81        &self,
82        crate_name: &str,
83        item_path: &str,
84        version: Option<&str>,
85    ) -> std::result::Result<String, CallToolError> {
86        // Try cache first
87        if let Some(cached) = self
88            .service
89            .doc_cache()
90            .get_item_docs(crate_name, item_path, version)
91            .await
92        {
93            return Ok(cached);
94        }
95
96        // Fetch from docs.rs
97        let url = Self::build_search_url(crate_name, item_path, version);
98        let html = self.service.fetch_html(&url, Some(TOOL_NAME)).await?;
99
100        // Extract search results
101        let docs = html::extract_search_results(&html, item_path);
102
103        // Cache result
104        self.service
105            .doc_cache()
106            .set_item_docs(crate_name, item_path, version, docs.clone())
107            .await
108            .map_err(|e| {
109                CallToolError::from_message(format!("[{TOOL_NAME}] Cache set failed: {e}"))
110            })?;
111
112        Ok(docs)
113    }
114
115    /// Get item documentation as plain text
116    async fn fetch_item_docs_as_text(
117        &self,
118        crate_name: &str,
119        item_path: &str,
120        version: Option<&str>,
121    ) -> std::result::Result<String, CallToolError> {
122        let url = Self::build_search_url(crate_name, item_path, version);
123        let html = self.service.fetch_html(&url, Some(TOOL_NAME)).await?;
124        Ok(format!(
125            "Search results: {}\n\n{}",
126            item_path,
127            html::html_to_text(&html)
128        ))
129    }
130
131    /// Get item documentation as raw HTML
132    async fn fetch_item_docs_as_html(
133        &self,
134        crate_name: &str,
135        item_path: &str,
136        version: Option<&str>,
137    ) -> std::result::Result<String, CallToolError> {
138        let url = Self::build_search_url(crate_name, item_path, version);
139        self.service.fetch_html(&url, Some(TOOL_NAME)).await
140    }
141}
142
143#[async_trait]
144impl Tool for LookupItemToolImpl {
145    fn definition(&self) -> rust_mcp_sdk::schema::Tool {
146        LookupItemTool::tool()
147    }
148
149    async fn execute(
150        &self,
151        arguments: serde_json::Value,
152    ) -> std::result::Result<
153        rust_mcp_sdk::schema::CallToolResult,
154        rust_mcp_sdk::schema::CallToolError,
155    > {
156        let params: LookupItemTool = serde_json::from_value(arguments).map_err(|e| {
157            rust_mcp_sdk::schema::CallToolError::invalid_arguments(
158                "lookup_item",
159                Some(format!("Parameter parsing failed: {e}")),
160            )
161        })?;
162
163        let format = super::parse_format(params.format.as_deref()).map_err(|_| {
164            rust_mcp_sdk::schema::CallToolError::invalid_arguments(
165                "lookup_item",
166                Some("Invalid format".to_string()),
167            )
168        })?;
169        let content = match format {
170            super::Format::Text => {
171                self.fetch_item_docs_as_text(
172                    &params.crate_name,
173                    &params.item_path,
174                    params.version.as_deref(),
175                )
176                .await?
177            }
178            super::Format::Html => {
179                self.fetch_item_docs_as_html(
180                    &params.crate_name,
181                    &params.item_path,
182                    params.version.as_deref(),
183                )
184                .await?
185            }
186            super::Format::Json => {
187                return Err(rust_mcp_sdk::schema::CallToolError::invalid_arguments(
188                    "lookup_item",
189                    Some("JSON format is not supported by this tool".to_string()),
190                ))
191            }
192            super::Format::Markdown => {
193                self.fetch_item_docs(
194                    &params.crate_name,
195                    &params.item_path,
196                    params.version.as_deref(),
197                )
198                .await?
199            }
200        };
201
202        Ok(rust_mcp_sdk::schema::CallToolResult::text_content(vec![
203            content.into(),
204        ]))
205    }
206}
207
208impl Default for LookupItemToolImpl {
209    fn default() -> Self {
210        Self::new(Arc::new(super::DocService::default()))
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use serial_test::serial;
218
219    #[test]
220    #[serial]
221    fn test_build_search_url_without_version() {
222        std::env::set_var("CRATES_DOCS_DOCS_RS_URL", "https://docs.rs");
223        let url = LookupItemToolImpl::build_search_url("serde", "Serialize", None);
224        assert_eq!(url, "https://docs.rs/serde/?search=Serialize");
225        std::env::remove_var("CRATES_DOCS_DOCS_RS_URL");
226    }
227
228    #[test]
229    #[serial]
230    fn test_build_search_url_with_version() {
231        std::env::set_var("CRATES_DOCS_DOCS_RS_URL", "https://docs.rs");
232        let url = LookupItemToolImpl::build_search_url("serde", "Serialize", Some("1.0.0"));
233        assert_eq!(url, "https://docs.rs/serde/1.0.0/?search=Serialize");
234        std::env::remove_var("CRATES_DOCS_DOCS_RS_URL");
235    }
236
237    #[test]
238    #[serial]
239    fn test_build_search_url_encodes_special_chars() {
240        std::env::set_var("CRATES_DOCS_DOCS_RS_URL", "https://docs.rs");
241        let url = LookupItemToolImpl::build_search_url("std", "collections::HashMap", None);
242        assert!(url.contains("collections%3A%3AHashMap"));
243        std::env::remove_var("CRATES_DOCS_DOCS_RS_URL");
244    }
245}