Skip to main content

docs_mcp/tools/
crate_list.rs

1use rmcp::{ErrorData, model::CallToolResult};
2use rmcp::model::Content;
3use serde::{Deserialize, Serialize};
4use rmcp::schemars::{self, JsonSchema};
5
6use crate::cratesio::CrateInfo;
7use super::AppState;
8
9#[derive(Serialize)]
10struct CrateListEntry<'a> {
11    name: &'a str,
12    description: Option<&'a str>,
13    version: Option<&'a str>,
14    newest_version: Option<&'a str>,
15    downloads: u64,
16    recent_downloads: Option<u64>,
17    updated_at: &'a str,
18    repository: Option<&'a str>,
19}
20
21impl<'a> From<&'a CrateInfo> for CrateListEntry<'a> {
22    fn from(c: &'a CrateInfo) -> Self {
23        Self {
24            name: &c.name,
25            description: c.description.as_deref(),
26            version: c.max_stable_version.as_deref().or(c.max_version.as_deref()),
27            newest_version: c.newest_version.as_deref(),
28            downloads: c.downloads,
29            recent_downloads: c.recent_downloads,
30            updated_at: &c.updated_at,
31            repository: c.repository.as_deref(),
32        }
33    }
34}
35
36#[derive(Debug, Deserialize, JsonSchema)]
37pub struct CrateListParams {
38    /// Free-text search query (e.g. "async http client")
39    pub query: Option<String>,
40    /// Filter by crates.io category slug (e.g. "web-programming")
41    pub category: Option<String>,
42    /// Filter by crates.io keyword tag
43    pub keyword: Option<String>,
44    /// Sort order: "relevance" (default), "downloads", "recent-downloads", "recent-updates", "alphabetical"
45    pub sort: Option<String>,
46    /// Page number (1-indexed, default: 1)
47    pub page: Option<u32>,
48    /// Results per page (max 100, default: 10)
49    pub per_page: Option<u32>,
50}
51
52pub async fn execute(state: &AppState, params: CrateListParams) -> Result<CallToolResult, ErrorData> {
53    let query = params.query.as_deref().unwrap_or("");
54    let page = params.page.unwrap_or(1).max(1);
55    let per_page = params.per_page.unwrap_or(10).min(100);
56
57    let client = crate::cratesio::CratesIoClient::new(&state.client, &state.cache);
58    let result = client
59        .search(
60            query,
61            params.category.as_deref(),
62            params.keyword.as_deref(),
63            params.sort.as_deref(),
64            page,
65            per_page,
66        )
67        .await
68        .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
69
70    let entries: Vec<CrateListEntry> = result.crates.iter().map(CrateListEntry::from).collect();
71    let output = serde_json::json!({ "crates": entries, "total": result.meta.total });
72    let json = serde_json::to_string_pretty(&output)
73        .map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
74
75    Ok(CallToolResult::success(vec![Content::text(json)]))
76}