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 .map_err(|e| CallToolError::from_message(format!("Cache set failed: {e}")))?;
126
127 Ok(crates)
128 }
129}
130
131#[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
142fn 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
194fn 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 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); let crates = self.search_crates(¶ms.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}