1#![allow(missing_docs)]
8
9use 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#[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#[derive(Debug, Clone, Deserialize, Serialize, rust_mcp_sdk::macros::JsonSchema)]
45pub struct LookupCrateTool {
46 #[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 #[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 #[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
69pub struct LookupCrateToolImpl {
74 service: Arc<DocService>,
76}
77
78impl LookupCrateToolImpl {
79 #[must_use]
81 pub fn new(service: Arc<DocService>) -> Self {
82 Self { service }
83 }
84
85 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 async fn fetch_crate_docs(
123 &self,
124 crate_name: &str,
125 version: Option<&str>,
126 ) -> std::result::Result<Arc<str>, CallToolError> {
127 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 let docs: Arc<str> = Arc::from(html::extract_documentation(&html).into_boxed_str());
141
142 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 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 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(¶ms.crate_name, params.version.as_deref())
203 .await?
204 }
205 super::Format::Html => {
206 self.fetch_crate_docs_as_html(¶ms.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(¶ms.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}