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
12const TOOL_NAME: &str = "lookup_crate";
13
14#[rust_mcp_sdk::macros::mcp_tool(
16 name = "lookup_crate",
17 title = "Lookup Crate Documentation",
18 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.",
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 LookupCrateTool {
32 #[json_schema(
34 title = "Crate Name",
35 description = "Crate name to lookup, e.g.: serde, tokio, reqwest"
36 )]
37 pub crate_name: String,
38
39 #[json_schema(
41 title = "Version",
42 description = "Crate version, e.g.: 1.0.0. Uses latest version if not specified"
43 )]
44 pub version: Option<String>,
45
46 #[json_schema(
48 title = "Output Format",
49 description = "Output format: markdown (default), text (plain text), html",
50 default = "markdown"
51 )]
52 pub format: Option<String>,
53}
54
55pub struct LookupCrateToolImpl {
57 service: Arc<DocService>,
58}
59
60impl LookupCrateToolImpl {
61 #[must_use]
63 pub fn new(service: Arc<DocService>) -> Self {
64 Self { service }
65 }
66
67 fn build_url(crate_name: &str, version: Option<&str>) -> String {
69 super::build_docs_url(crate_name, version)
70 }
71
72 async fn fetch_crate_docs(
74 &self,
75 crate_name: &str,
76 version: Option<&str>,
77 ) -> std::result::Result<String, CallToolError> {
78 if let Some(cached) = self
80 .service
81 .doc_cache()
82 .get_crate_docs(crate_name, version)
83 .await
84 {
85 return Ok(cached);
86 }
87
88 let url = Self::build_url(crate_name, version);
90 let html = self.service.fetch_html(&url, Some(TOOL_NAME)).await?;
91
92 let docs = html::extract_documentation(&html);
94
95 self.service
97 .doc_cache()
98 .set_crate_docs(crate_name, version, docs.clone())
99 .await
100 .map_err(|e| {
101 CallToolError::from_message(format!("[{TOOL_NAME}] Cache set failed: {e}"))
102 })?;
103
104 Ok(docs)
105 }
106
107 async fn fetch_crate_docs_as_text(
109 &self,
110 crate_name: &str,
111 version: Option<&str>,
112 ) -> std::result::Result<String, CallToolError> {
113 let url = Self::build_url(crate_name, version);
114 let html = self.service.fetch_html(&url, Some(TOOL_NAME)).await?;
115 Ok(html::html_to_text(&html))
116 }
117
118 async fn fetch_crate_docs_as_html(
120 &self,
121 crate_name: &str,
122 version: Option<&str>,
123 ) -> std::result::Result<String, CallToolError> {
124 let url = Self::build_url(crate_name, version);
125 self.service.fetch_html(&url, Some(TOOL_NAME)).await
126 }
127}
128
129#[async_trait]
130impl Tool for LookupCrateToolImpl {
131 fn definition(&self) -> rust_mcp_sdk::schema::Tool {
132 LookupCrateTool::tool()
133 }
134
135 async fn execute(
136 &self,
137 arguments: serde_json::Value,
138 ) -> std::result::Result<
139 rust_mcp_sdk::schema::CallToolResult,
140 rust_mcp_sdk::schema::CallToolError,
141 > {
142 let params: LookupCrateTool = serde_json::from_value(arguments).map_err(|e| {
143 rust_mcp_sdk::schema::CallToolError::invalid_arguments(
144 "lookup_crate",
145 Some(format!("Parameter parsing failed: {e}")),
146 )
147 })?;
148
149 let format = super::parse_format(params.format.as_deref()).map_err(|_| {
150 rust_mcp_sdk::schema::CallToolError::invalid_arguments(
151 "lookup_crate",
152 Some("Invalid format".to_string()),
153 )
154 })?;
155 let content = match format {
156 super::Format::Text => {
157 self.fetch_crate_docs_as_text(¶ms.crate_name, params.version.as_deref())
158 .await?
159 }
160 super::Format::Html => {
161 self.fetch_crate_docs_as_html(¶ms.crate_name, params.version.as_deref())
162 .await?
163 }
164 super::Format::Json => {
165 return Err(rust_mcp_sdk::schema::CallToolError::invalid_arguments(
166 "lookup_crate",
167 Some("JSON format is not supported by this tool".to_string()),
168 ))
169 }
170 super::Format::Markdown => {
171 self.fetch_crate_docs(¶ms.crate_name, params.version.as_deref())
172 .await?
173 }
174 };
175
176 Ok(rust_mcp_sdk::schema::CallToolResult::text_content(vec![
177 content.into(),
178 ]))
179 }
180}
181
182impl Default for LookupCrateToolImpl {
183 fn default() -> Self {
184 Self::new(Arc::new(super::DocService::default()))
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use serial_test::serial;
192
193 #[test]
194 #[serial]
195 fn test_build_url_without_version() {
196 std::env::set_var("CRATES_DOCS_DOCS_RS_URL", "https://docs.rs");
197 let url = LookupCrateToolImpl::build_url("serde", None);
198 assert_eq!(url, "https://docs.rs/serde/");
199 std::env::remove_var("CRATES_DOCS_DOCS_RS_URL");
200 }
201
202 #[test]
203 #[serial]
204 fn test_build_url_with_version() {
205 std::env::set_var("CRATES_DOCS_DOCS_RS_URL", "https://docs.rs");
206 let url = LookupCrateToolImpl::build_url("serde", Some("1.0.0"));
207 assert_eq!(url, "https://docs.rs/serde/1.0.0/");
208 std::env::remove_var("CRATES_DOCS_DOCS_RS_URL");
209 }
210
211 #[test]
212 #[serial]
213 fn test_build_url_with_custom_base() {
214 std::env::set_var("CRATES_DOCS_DOCS_RS_URL", "http://mock-server");
215 let url = LookupCrateToolImpl::build_url("serde", None);
216 assert_eq!(url, "http://mock-server/serde/");
217 std::env::remove_var("CRATES_DOCS_DOCS_RS_URL");
218 }
219}