1#![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#[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 #[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 #[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 #[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
54pub struct SearchCratesToolImpl {
56 service: Arc<super::DocService>,
57}
58
59impl SearchCratesToolImpl {
60 #[must_use]
62 pub fn new(service: Arc<super::DocService>) -> Self {
63 Self { service }
64 }
65
66 async fn search_crates(
68 &self,
69 query: &str,
70 limit: u32,
71 ) -> std::result::Result<Vec<CrateInfo>, CallToolError> {
72 let cache_key = format!("search:{query}:{limit}");
74
75 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 let url = format!(
83 "https://crates.io/api/v1/crates?q={}&per_page={}",
84 urlencoding::encode(query),
85 limit
86 );
87
88 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 let crates = parse_crates_response(&json, limit as usize);
112
113 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#[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
141fn 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
193fn 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 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); let crates = self.search_crates(¶ms.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}