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
126        Ok(crates)
127    }
128}
129
130/// Crate 信息
131#[derive(Debug, Clone, Serialize, Deserialize)]
132struct CrateInfo {
133    name: String,
134    description: Option<String>,
135    version: String,
136    downloads: u64,
137    repository: Option<String>,
138    documentation: Option<String>,
139}
140
141/// 解析 crates.io API 响应
142fn parse_crates_response(json: &serde_json::Value, limit: usize) -> Vec<CrateInfo> {
143    let mut crates = Vec::new();
144
145    if let Some(crates_array) = json.get("crates").and_then(|c| c.as_array()) {
146        for crate_item in crates_array.iter().take(limit) {
147            let name = crate_item
148                .get("name")
149                .and_then(|n| n.as_str())
150                .unwrap_or("Unknown")
151                .to_string();
152
153            let description = crate_item
154                .get("description")
155                .and_then(|d| d.as_str())
156                .map(std::string::ToString::to_string);
157
158            let version = crate_item
159                .get("max_version")
160                .and_then(|v| v.as_str())
161                .unwrap_or("0.0.0")
162                .to_string();
163
164            let downloads = crate_item
165                .get("downloads")
166                .and_then(serde_json::Value::as_u64)
167                .unwrap_or(0);
168
169            let repository = crate_item
170                .get("repository")
171                .and_then(|r| r.as_str())
172                .map(std::string::ToString::to_string);
173
174            let documentation = crate_item
175                .get("documentation")
176                .and_then(|d| d.as_str())
177                .map(std::string::ToString::to_string);
178
179            crates.push(CrateInfo {
180                name,
181                description,
182                version,
183                downloads,
184                repository,
185                documentation,
186            });
187        }
188    }
189
190    crates
191}
192
193/// 格式化搜索结果
194fn format_search_results(crates: &[CrateInfo], format: &str) -> String {
195    match format {
196        "json" => serde_json::to_string_pretty(crates).unwrap_or_else(|_| "[]".to_string()),
197        "markdown" => {
198            use std::fmt::Write;
199            let mut output = String::from("# 搜索结果\n\n");
200
201            for (i, crate_info) in crates.iter().enumerate() {
202                writeln!(output, "## {}. {}", i + 1, crate_info.name).unwrap();
203                writeln!(output, "**版本**: {}", crate_info.version).unwrap();
204                writeln!(output, "**下载量**: {}", crate_info.downloads).unwrap();
205
206                if let Some(desc) = &crate_info.description {
207                    writeln!(output, "**描述**: {desc}").unwrap();
208                }
209
210                if let Some(repo) = &crate_info.repository {
211                    writeln!(output, "**仓库**: [链接]({repo})").unwrap();
212                }
213
214                if let Some(docs) = &crate_info.documentation {
215                    writeln!(output, "**文档**: [链接]({docs})").unwrap();
216                }
217
218                writeln!(
219                    output,
220                    "**Docs.rs**: [https://docs.rs/{}/](https://docs.rs/{}/)\n",
221                    crate_info.name, crate_info.name
222                )
223                .unwrap();
224            }
225
226            output
227        }
228        "text" => {
229            use std::fmt::Write;
230            let mut output = String::new();
231
232            for (i, crate_info) in crates.iter().enumerate() {
233                writeln!(output, "{}. {}", i + 1, crate_info.name).unwrap();
234                writeln!(output, "   版本: {}", crate_info.version).unwrap();
235                writeln!(output, "   下载量: {}", crate_info.downloads).unwrap();
236
237                if let Some(desc) = &crate_info.description {
238                    writeln!(output, "   描述: {desc}").unwrap();
239                }
240
241                writeln!(output, "   Docs.rs: https://docs.rs/{}/", crate_info.name).unwrap();
242                writeln!(output).unwrap();
243            }
244
245            output
246        }
247        _ => {
248            // 默认使用 markdown
249            format_search_results(crates, "markdown")
250        }
251    }
252}
253
254#[async_trait]
255impl Tool for SearchCratesToolImpl {
256    fn definition(&self) -> rust_mcp_sdk::schema::Tool {
257        SearchCratesTool::tool()
258    }
259
260    async fn execute(
261        &self,
262        arguments: serde_json::Value,
263    ) -> std::result::Result<
264        rust_mcp_sdk::schema::CallToolResult,
265        rust_mcp_sdk::schema::CallToolError,
266    > {
267        let params: SearchCratesTool = serde_json::from_value(arguments).map_err(|e| {
268            rust_mcp_sdk::schema::CallToolError::invalid_arguments(
269                "search_crates",
270                Some(format!("参数解析失败: {e}")),
271            )
272        })?;
273
274        let limit = params.limit.unwrap_or(10).min(100); // 限制最大100个结果
275        let crates = self.search_crates(&params.query, limit).await?;
276
277        let format = params.format.unwrap_or_else(|| "markdown".to_string());
278        let content = format_search_results(&crates, &format);
279
280        Ok(rust_mcp_sdk::schema::CallToolResult::text_content(vec![
281            content.into(),
282        ]))
283    }
284}
285
286impl Default for SearchCratesToolImpl {
287    fn default() -> Self {
288        Self::new(Arc::new(super::DocService::default()))
289    }
290}