rust_docs_mcp/
service.rs

1use rmcp::handler::server::wrapper::Parameters;
2use std::path::PathBuf;
3use std::sync::Arc;
4use tokio::sync::RwLock;
5
6use anyhow::Result;
7use rmcp::schemars::{self, JsonSchema};
8use rmcp::{
9    ErrorData, RoleServer, ServerHandler,
10    handler::server::{router::prompt::PromptRouter, router::tool::ToolRouter},
11    model::{
12        GetPromptRequestParam, GetPromptResult, ListPromptsResult, PaginatedRequestParam,
13        PromptMessage, PromptMessageRole, ServerCapabilities, ServerInfo,
14    },
15    prompt, prompt_handler, prompt_router,
16    service::RequestContext,
17    tool, tool_handler, tool_router,
18};
19
20use serde::{Deserialize, Serialize};
21
22use crate::analysis::tools::{AnalysisTools, AnalyzeCrateStructureParams};
23use crate::cache::{
24    CrateCache,
25    tools::{
26        CacheCrateFromCratesIOParams, CacheCrateFromGitHubParams, CacheCrateFromLocalParams,
27        CacheTools, GetCratesMetadataParams, ListCachedCratesParams, ListCrateVersionsParams,
28        RemoveCrateParams,
29    },
30};
31use crate::deps::tools::{DepsTools, GetDependenciesParams};
32use crate::docs::tools::{
33    DocsTools, GetItemDetailsParams, GetItemDocsParams, GetItemSourceParams, ListItemsParams,
34    SearchItemsParams, SearchItemsPreviewParams,
35};
36use crate::search::tools::{SearchItemsFuzzyParams, SearchTools};
37
38#[derive(Debug, Serialize, Deserialize, JsonSchema)]
39struct CacheDependenciesArgs {
40    /// Path to the Cargo.toml file or project directory (defaults to current working directory if not specified)
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub project_path: Option<String>,
43
44    /// Optional workspace member name to focus on specific member dependencies
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub member_name: Option<String>,
47
48    /// Whether to force re-cache dependencies that are already cached
49    #[serde(
50        default,
51        deserialize_with = "crate::util::deserialize_bool_from_anything"
52    )]
53    pub force_update: bool,
54}
55
56#[derive(Debug, Clone)]
57pub struct RustDocsService {
58    tool_router: ToolRouter<Self>,
59    prompt_router: PromptRouter<Self>,
60    cache_tools: CacheTools,
61    docs_tools: DocsTools,
62    deps_tools: DepsTools,
63    analysis_tools: AnalysisTools,
64    search_tools: SearchTools,
65}
66
67#[tool_router]
68impl RustDocsService {
69    pub fn new(cache_dir: Option<PathBuf>) -> Result<Self> {
70        let cache = Arc::new(RwLock::new(CrateCache::new(cache_dir)?));
71
72        Ok(Self {
73            tool_router: Self::tool_router(),
74            prompt_router: Self::prompt_router(),
75            cache_tools: CacheTools::new(cache.clone()),
76            docs_tools: DocsTools::new(cache.clone()),
77            deps_tools: DepsTools::new(cache.clone()),
78            analysis_tools: AnalysisTools::new(cache.clone()),
79            search_tools: SearchTools::new(cache),
80        })
81    }
82
83    // Cache tools
84    #[tool(
85        description = "Download and cache a specific crate version from crates.io for offline use. This happens automatically when using other tools, but use this to pre-cache crates. Useful for preparing offline access or ensuring a crate is available before searching."
86    )]
87    pub async fn cache_crate_from_cratesio(
88        &self,
89        Parameters(params): Parameters<CacheCrateFromCratesIOParams>,
90    ) -> String {
91        let output = self.cache_tools.cache_crate_from_cratesio(params).await;
92        output.to_json()
93    }
94
95    #[tool(
96        description = "Download and cache a specific crate version from GitHub for offline use. Supports cloning from any GitHub repository URL. You must specify either a branch OR a tag (but not both). The crate will be cached using the branch/tag name as the version."
97    )]
98    pub async fn cache_crate_from_github(
99        &self,
100        Parameters(params): Parameters<CacheCrateFromGitHubParams>,
101    ) -> String {
102        let output = self.cache_tools.cache_crate_from_github(params).await;
103        output.to_json()
104    }
105
106    #[tool(
107        description = "Cache a specific crate version from a local file system path. Supports absolute paths, home paths (~), and relative paths. The specified directory must contain a Cargo.toml file."
108    )]
109    pub async fn cache_crate_from_local(
110        &self,
111        Parameters(params): Parameters<CacheCrateFromLocalParams>,
112    ) -> String {
113        let output = self.cache_tools.cache_crate_from_local(params).await;
114        output.to_json()
115    }
116
117    #[tool(
118        description = "Remove a cached crate version from local storage. Use to free up disk space or remove outdated versions. This only affects the local cache - the crate can be re-downloaded later if needed."
119    )]
120    pub async fn remove_crate(&self, Parameters(params): Parameters<RemoveCrateParams>) -> String {
121        match self.cache_tools.remove_crate(params).await {
122            Ok(output) => output.to_json(),
123            Err(error) => error.to_json(),
124        }
125    }
126
127    #[tool(
128        description = "List all locally cached crates with their versions and sizes. Use to see what crates are available offline and how much disk space they use. Shows cache metadata including when each crate was cached."
129    )]
130    pub async fn list_cached_crates(
131        &self,
132        Parameters(_params): Parameters<ListCachedCratesParams>,
133    ) -> String {
134        match self.cache_tools.list_cached_crates().await {
135            Ok(output) => output.to_json(),
136            Err(error) => error.to_json(),
137        }
138    }
139
140    #[tool(
141        description = "List all locally cached versions of a crate. Use to check what versions are available offline without downloading. Useful before calling other tools to verify if a version needs to be cached first."
142    )]
143    pub async fn list_crate_versions(
144        &self,
145        Parameters(params): Parameters<ListCrateVersionsParams>,
146    ) -> String {
147        match self.cache_tools.list_crate_versions(params).await {
148            Ok(output) => output.to_json(),
149            Err(error) => error.to_json(),
150        }
151    }
152
153    #[tool(
154        description = "Get metadata for multiple crates and their workspace members in a single call. Use this to efficiently check the caching and analysis status of multiple crates at once. Returns metadata including caching status, analysis status, and cache sizes for each requested crate and member."
155    )]
156    pub async fn get_crates_metadata(
157        &self,
158        Parameters(params): Parameters<GetCratesMetadataParams>,
159    ) -> String {
160        let output = self.cache_tools.get_crates_metadata(params).await;
161        output.to_json()
162    }
163
164    // Docs tools
165    #[tool(
166        description = "List all items in a crate's documentation. Use when browsing a crate's contents without a specific search term. Returns full item details including documentation. For large crates, consider using search_items_preview for a lighter response that only includes names and types. For workspace crates, specify the member parameter with the member path (e.g., 'crates/rmcp')."
167    )]
168    pub async fn list_crate_items(
169        &self,
170        Parameters(params): Parameters<ListItemsParams>,
171    ) -> String {
172        match self.docs_tools.list_crate_items(params).await {
173            Ok(output) => output.to_json(),
174            Err(error) => error.to_json(),
175        }
176    }
177
178    #[tool(
179        description = "Search for items by name pattern in a crate. Use when looking for specific functions, types, or modules. Returns FULL details including documentation. WARNING: May exceed token limits for large results. Use search_items_preview first for exploration, then get_item_details for specific items. For workspace crates, specify the member parameter with the member path (e.g., 'crates/rmcp')."
180    )]
181    pub async fn search_items(&self, Parameters(params): Parameters<SearchItemsParams>) -> String {
182        match self.docs_tools.search_items(params).await {
183            Ok(output) => output.to_json(),
184            Err(error) => error.to_json(),
185        }
186    }
187
188    #[tool(
189        description = "Search for items by name pattern in a crate - PREVIEW MODE. Use this FIRST when searching to avoid token limits. Returns only id, name, kind, and path. Once you find items of interest, use get_item_details to fetch full documentation. This is the recommended search method for exploration. For workspace crates, specify the member parameter with the member path (e.g., 'crates/rmcp')."
190    )]
191    pub async fn search_items_preview(
192        &self,
193        Parameters(params): Parameters<SearchItemsPreviewParams>,
194    ) -> String {
195        match self.docs_tools.search_items_preview(params).await {
196            Ok(output) => output.to_json(),
197            Err(error) => error.to_json(),
198        }
199    }
200
201    #[tool(
202        description = "Get detailed information about a specific item by ID. Use after search_items_preview to fetch full details including documentation, signatures, fields, methods, etc. The item_id comes from search results. This is the recommended way to get complete information about a specific item. For workspace crates, specify the member parameter with the member path (e.g., 'crates/rmcp')."
203    )]
204    pub async fn get_item_details(
205        &self,
206        Parameters(params): Parameters<GetItemDetailsParams>,
207    ) -> String {
208        self.docs_tools.get_item_details(params).await.to_json()
209    }
210
211    #[tool(
212        description = "Get ONLY the documentation string for a specific item. Use when you need just the docs without other details. More efficient than get_item_details if you only need the documentation text. Returns null if no documentation exists. For workspace crates, specify the member parameter with the member path (e.g., 'crates/rmcp')."
213    )]
214    pub async fn get_item_docs(&self, Parameters(params): Parameters<GetItemDocsParams>) -> String {
215        match self.docs_tools.get_item_docs(params).await {
216            Ok(output) => output.to_json(),
217            Err(error) => error.to_json(),
218        }
219    }
220
221    #[tool(
222        description = "Get the source code for a specific item. Returns the actual source code with optional context lines. Use after finding items of interest to view their implementation. The source location is also included in get_item_details responses. For workspace crates, specify the member parameter with the member path (e.g., 'crates/rmcp')."
223    )]
224    pub async fn get_item_source(
225        &self,
226        Parameters(params): Parameters<GetItemSourceParams>,
227    ) -> String {
228        self.docs_tools.get_item_source(params).await.to_json()
229    }
230
231    // Deps tools
232    #[tool(
233        description = "Get dependency information for a crate. Returns direct dependencies by default, with option to include full dependency tree. Use this to understand what a crate depends on, check for version conflicts, or explore the dependency graph. For workspace crates, specify the member parameter with the member path (e.g., 'crates/rmcp')."
234    )]
235    pub async fn get_dependencies(
236        &self,
237        Parameters(params): Parameters<GetDependenciesParams>,
238    ) -> String {
239        match self.deps_tools.get_dependencies(params).await {
240            Ok(output) => output.to_json(),
241            Err(error) => error.to_json(),
242        }
243    }
244
245    // Analysis tools
246    #[tool(
247        description = "View the hierarchical structure as a tree to view the high level components of the crate. This is a good starting point to have a high-level overview of the crate's organization. This will allow you to narrow down your search confidently to find what you are looking for."
248    )]
249    pub async fn structure(
250        &self,
251        Parameters(params): Parameters<AnalyzeCrateStructureParams>,
252    ) -> String {
253        match self.analysis_tools.structure(params).await {
254            Ok(output) => output.to_json(),
255            Err(error) => error.to_json(),
256        }
257    }
258
259    // Search tools
260    #[tool(
261        description = "Perform fuzzy search on crate items with typo tolerance and semantic similarity. This provides more flexible searching compared to exact pattern matching, allowing you to find items even with typos or partial matches. The search indexes item names, documentation, and metadata using Tantivy full-text search engine. For workspace crates, specify the member parameter with the member path (e.g., 'crates/rmcp')."
262    )]
263    pub async fn search_items_fuzzy(
264        &self,
265        Parameters(params): Parameters<SearchItemsFuzzyParams>,
266    ) -> String {
267        match self.search_tools.search_items_fuzzy(params).await {
268            Ok(output) => output.to_json(),
269            Err(error) => error.to_json(),
270        }
271    }
272}
273
274#[prompt_router]
275impl RustDocsService {
276    #[prompt(
277        name = "cache_dependencies",
278        description = "Cache all dependencies from a Rust project's Cargo.toml"
279    )]
280    pub async fn cache_dependencies(
281        &self,
282        Parameters(args): Parameters<CacheDependenciesArgs>,
283        _ctx: RequestContext<RoleServer>,
284    ) -> Result<Vec<PromptMessage>, ErrorData> {
285        let messages = vec![
286            PromptMessage::new_text(
287                PromptMessageRole::User,
288                format!(
289                    "I need to cache all dependencies from the Rust project{}{}. \
290                    Please analyze the Cargo.toml file{} and cache every dependency using the rust-docs MCP caching tools.",
291                    args.project_path
292                        .as_ref()
293                        .map(|p| format!(" at {p}"))
294                        .unwrap_or_else(|| " in the current working directory".to_string()),
295                    args.member_name
296                        .as_ref()
297                        .map(|m| format!(" (focusing on member: {m})"))
298                        .unwrap_or_default(),
299                    if args.force_update {
300                        " and force update existing cached dependencies"
301                    } else {
302                        ""
303                    }
304                ),
305            ),
306            PromptMessage::new_text(
307                PromptMessageRole::Assistant,
308                format!(
309                    "I'll help you cache all dependencies from the project{}. \
310                    I'll read the Cargo.toml file{}, analyze all dependencies (including dev-dependencies), \
311                    and cache them using the appropriate rust-docs MCP tools.\n\n\
312                    First, I'll aggregate a list of all dependencies with their:\n\
313                    - Source (crates.io, GitHub, or local absolute path)\n\
314                    - Full semver version (e.g., 4.0.0 not 4.0 - if minor/patch are missing, fill with zeros)\n\n\
315                    Then I'll cache them using:\n\
316                    - For crates.io dependencies: cache_crate_from_cratesio\n\
317                    - For Git dependencies: cache_crate_from_github\n\
318                    - For local path dependencies: cache_crate_from_local\n\n\
319                    Let me start by examining the Cargo.toml file to identify all dependencies.",
320                    args.project_path
321                        .as_ref()
322                        .map(|p| format!(" at '{p}'"))
323                        .unwrap_or_else(|| " in the current working directory".to_string()),
324                    args.member_name
325                        .as_ref()
326                        .map(|m| format!(" (member: {m})"))
327                        .unwrap_or_default()
328                ),
329            ),
330        ];
331
332        Ok(messages)
333    }
334}
335
336#[tool_handler]
337#[prompt_handler]
338impl ServerHandler for RustDocsService {
339    fn get_info(&self) -> ServerInfo {
340        ServerInfo {
341            server_info: rmcp::model::Implementation {
342                name: "rust-docs-mcp".to_string(),
343                version: "0.1.1".to_string(),
344                title: None,
345                website_url: None,
346                icons: None,
347            },
348            capabilities: ServerCapabilities {
349                tools: Some(Default::default()),
350                prompts: Some(Default::default()),
351                ..Default::default()
352            },
353            instructions: Some(
354                "MCP server for analyzing crate structure and querying documentation, dependencies and source code. Use the structure tool to get a high-level overview of the crate's organization before narrowing down your search. Use list_cached_crates to see what crates are already cached and to easily find the crate or member from a workspace crate instead of guessing. Common workflow: search_items_preview to find items quickly by symbol name, then get_item_details to fetch full documentation. For more flexible searching, use search_items_fuzzy which supports typo tolerance and fuzzy matching. Use get_item_source to view the actual source code of items. Use get_dependencies to understand a crate's dependency graph.".to_string(),
355            ),
356            ..Default::default()
357        }
358    }
359}