crates_docs/tools/docs/
lookup_crate.rs1#![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#[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 #[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 #[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 #[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
53pub struct LookupCrateToolImpl {
55 service: Arc<DocService>,
56}
57
58impl LookupCrateToolImpl {
59 #[must_use]
61 pub fn new(service: Arc<DocService>) -> Self {
62 Self { service }
63 }
64
65 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 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 async fn fetch_crate_docs(
107 &self,
108 crate_name: &str,
109 version: Option<&str>,
110 ) -> std::result::Result<String, CallToolError> {
111 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 let url = Self::build_url(crate_name, version);
123 let html = self.fetch_html(&url).await?;
124
125 let docs = html::extract_documentation(&html);
127
128 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 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 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(¶ms.crate_name, params.version.as_deref())
184 .await?
185 }
186 "html" => {
187 self.fetch_crate_docs_as_html(¶ms.crate_name, params.version.as_deref())
188 .await?
189 }
190 _ => {
191 self.fetch_crate_docs(¶ms.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}