Skip to main content

crates_docs/tools/docs/
search.rs

1//! Search crates tool
2#![allow(missing_docs)]
3
4use crate::tools::Tool;
5use async_trait::async_trait;
6use rust_mcp_sdk::macros;
7use rust_mcp_sdk::schema::CallToolError;
8use serde::{Deserialize, Serialize};
9use std::sync::Arc;
10
11/// Search crates tool parameters
12#[macros::mcp_tool(
13    name = "search_crates",
14    title = "Search Crates",
15    description = "Search for Rust crates from crates.io. Returns a list of matching crates, including name, description, version, downloads, etc. Suitable for discovering and comparing available Rust libraries.",
16    destructive_hint = false,
17    idempotent_hint = true,
18    open_world_hint = false,
19    read_only_hint = true,
20    execution(task_support = "optional"),
21    icons = [
22        (src = "https://crates.io/favicon.ico", mime_type = "image/x-icon", sizes = ["32x32"], theme = "light"),
23        (src = "https://crates.io/favicon.ico", mime_type = "image/x-icon", sizes = ["32x32"], theme = "dark")
24    ]
25)]
26#[derive(Debug, Clone, Deserialize, Serialize, macros::JsonSchema)]
27pub struct SearchCratesTool {
28    /// Search query
29    #[json_schema(
30        title = "Search query",
31        description = "Search keywords, e.g.: web framework, async, http client, serialization"
32    )]
33    pub query: String,
34
35    /// Result count limit
36    #[json_schema(
37        title = "结果限制",
38        description = "Maximum number of results to return, range 1-100",
39        minimum = 1,
40        maximum = 100,
41        default = 10
42    )]
43    pub limit: Option<u32>,
44
45    /// 输出格式
46    #[json_schema(
47        title = "输出格式",
48        description = "Search result output format: markdown (default), text (plain text), json (raw JSON)",
49        default = "markdown"
50    )]
51    pub format: Option<String>,
52}
53
54/// Search crates tool实现
55pub struct SearchCratesToolImpl {
56    service: Arc<super::DocService>,
57}
58
59impl SearchCratesToolImpl {
60    /// Create a new tool instance
61    #[must_use]
62    pub fn new(service: Arc<super::DocService>) -> Self {
63        Self { service }
64    }
65
66    /// Search crates
67    async fn search_crates(
68        &self,
69        query: &str,
70        limit: u32,
71    ) -> std::result::Result<Vec<CrateInfo>, CallToolError> {
72        // Build cache key
73        let cache_key = format!("search:{query}:{limit}");
74
75        // Check cache
76        if let Some(cached) = self.service.cache().get(&cache_key).await {
77            return serde_json::from_str(&cached)
78                .map_err(|e| CallToolError::from_message(format!("Cache parsing failed: {e}")));
79        }
80
81        // Build crates.io API URL
82        let url = format!(
83            "https://crates.io/api/v1/crates?q={}&per_page={}",
84            urlencoding::encode(query),
85            limit
86        );
87
88        // 发送 HTTP 请求
89        let response = self
90            .service
91            .client()
92            .get(&url)
93            .header("User-Agent", format!("CratesDocsMCP/{}", crate::VERSION))
94            .send()
95            .await
96            .map_err(|e| CallToolError::from_message(format!("HTTP 请求失败: {e}")))?;
97
98        if !response.status().is_success() {
99            return Err(CallToolError::from_message(format!(
100                "Search failed, status code: {}",
101                response.status()
102            )));
103        }
104
105        let json: serde_json::Value = response
106            .json()
107            .await
108            .map_err(|e| CallToolError::from_message(format!("JSON 解析失败: {e}")))?;
109
110        // 解析响应
111        let crates = parse_crates_response(&json, limit as usize);
112
113        // 缓存结果(5分钟)
114        let cache_value = serde_json::to_string(&crates)
115            .map_err(|e| CallToolError::from_message(format!("序列化失败: {e}")))?;
116
117        self.service
118            .cache()
119            .set(
120                cache_key,
121                cache_value,
122                Some(std::time::Duration::from_secs(300)),
123            )
124            .await
125            .map_err(|e| CallToolError::from_message(format!("Cache set failed: {e}")))?;
126
127        Ok(crates)
128    }
129}
130
131/// Crate 信息
132#[derive(Debug, Clone, Serialize, Deserialize)]
133struct CrateInfo {
134    name: String,
135    description: Option<String>,
136    version: String,
137    downloads: u64,
138    repository: Option<String>,
139    documentation: Option<String>,
140}
141
142/// 解析 crates.io API 响应
143fn parse_crates_response(json: &serde_json::Value, limit: usize) -> Vec<CrateInfo> {
144    let mut crates = Vec::new();
145
146    if let Some(crates_array) = json.get("crates").and_then(|c| c.as_array()) {
147        for crate_item in crates_array.iter().take(limit) {
148            let name = crate_item
149                .get("name")
150                .and_then(|n| n.as_str())
151                .unwrap_or("Unknown")
152                .to_string();
153
154            let description = crate_item
155                .get("description")
156                .and_then(|d| d.as_str())
157                .map(std::string::ToString::to_string);
158
159            let version = crate_item
160                .get("max_version")
161                .and_then(|v| v.as_str())
162                .unwrap_or("0.0.0")
163                .to_string();
164
165            let downloads = crate_item
166                .get("downloads")
167                .and_then(serde_json::Value::as_u64)
168                .unwrap_or(0);
169
170            let repository = crate_item
171                .get("repository")
172                .and_then(|r| r.as_str())
173                .map(std::string::ToString::to_string);
174
175            let documentation = crate_item
176                .get("documentation")
177                .and_then(|d| d.as_str())
178                .map(std::string::ToString::to_string);
179
180            crates.push(CrateInfo {
181                name,
182                description,
183                version,
184                downloads,
185                repository,
186                documentation,
187            });
188        }
189    }
190
191    crates
192}
193
194/// 格式化搜索结果
195fn format_search_results(crates: &[CrateInfo], format: &str) -> String {
196    match format {
197        "json" => serde_json::to_string_pretty(crates).unwrap_or_else(|_| "[]".to_string()),
198        "markdown" => {
199            use std::fmt::Write;
200            let mut output = String::from("# 搜索结果\n\n");
201
202            for (i, crate_info) in crates.iter().enumerate() {
203                writeln!(output, "## {}. {}", i + 1, crate_info.name).unwrap();
204                writeln!(output, "**版本**: {}", crate_info.version).unwrap();
205                writeln!(output, "**下载量**: {}", crate_info.downloads).unwrap();
206
207                if let Some(desc) = &crate_info.description {
208                    writeln!(output, "**描述**: {desc}").unwrap();
209                }
210
211                if let Some(repo) = &crate_info.repository {
212                    writeln!(output, "**仓库**: [链接]({repo})").unwrap();
213                }
214
215                if let Some(docs) = &crate_info.documentation {
216                    writeln!(output, "**文档**: [链接]({docs})").unwrap();
217                }
218
219                writeln!(
220                    output,
221                    "**Docs.rs**: [https://docs.rs/{}/](https://docs.rs/{}/)\n",
222                    crate_info.name, crate_info.name
223                )
224                .unwrap();
225            }
226
227            output
228        }
229        "text" => {
230            use std::fmt::Write;
231            let mut output = String::new();
232
233            for (i, crate_info) in crates.iter().enumerate() {
234                writeln!(output, "{}. {}", i + 1, crate_info.name).unwrap();
235                writeln!(output, "   版本: {}", crate_info.version).unwrap();
236                writeln!(output, "   下载量: {}", crate_info.downloads).unwrap();
237
238                if let Some(desc) = &crate_info.description {
239                    writeln!(output, "   描述: {desc}").unwrap();
240                }
241
242                writeln!(output, "   Docs.rs: https://docs.rs/{}/", crate_info.name).unwrap();
243                writeln!(output).unwrap();
244            }
245
246            output
247        }
248        _ => {
249            // 默认使用 markdown
250            format_search_results(crates, "markdown")
251        }
252    }
253}
254
255#[async_trait]
256impl Tool for SearchCratesToolImpl {
257    fn definition(&self) -> rust_mcp_sdk::schema::Tool {
258        SearchCratesTool::tool()
259    }
260
261    async fn execute(
262        &self,
263        arguments: serde_json::Value,
264    ) -> std::result::Result<
265        rust_mcp_sdk::schema::CallToolResult,
266        rust_mcp_sdk::schema::CallToolError,
267    > {
268        let params: SearchCratesTool = serde_json::from_value(arguments).map_err(|e| {
269            rust_mcp_sdk::schema::CallToolError::invalid_arguments(
270                "search_crates",
271                Some(format!("参数解析失败: {e}")),
272            )
273        })?;
274
275        let limit = params.limit.unwrap_or(10).min(100); // 限制最大100个结果
276        let crates = self.search_crates(&params.query, limit).await?;
277
278        let format = params.format.unwrap_or_else(|| "markdown".to_string());
279        let content = format_search_results(&crates, &format);
280
281        Ok(rust_mcp_sdk::schema::CallToolResult::text_content(vec![
282            content.into(),
283        ]))
284    }
285}
286
287impl Default for SearchCratesToolImpl {
288    fn default() -> Self {
289        Self::new(Arc::new(super::DocService::default()))
290    }
291}