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, ListCrateVersionsParams, RemoveCrateParams,
28    },
29};
30use crate::deps::tools::{DepsTools, GetDependenciesParams};
31use crate::docs::tools::{
32    DocsTools, GetItemDetailsParams, GetItemDocsParams, GetItemSourceParams, ListItemsParams,
33    SearchItemsParams, SearchItemsPreviewParams,
34};
35use crate::search::tools::{SearchItemsFuzzyParams, SearchTools};
36
37#[derive(Debug, Serialize, Deserialize, JsonSchema)]
38struct CacheDependenciesArgs {
39    /// Path to the Cargo.toml file or project directory (defaults to current working directory if not specified)
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub project_path: Option<String>,
42
43    /// Optional workspace member name to focus on specific member dependencies
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub member_name: Option<String>,
46
47    /// Whether to force re-cache dependencies that are already cached
48    #[serde(
49        default,
50        deserialize_with = "crate::util::deserialize_bool_from_anything"
51    )]
52    pub force_update: bool,
53}
54
55#[derive(Debug, Clone)]
56pub struct RustDocsService {
57    tool_router: ToolRouter<Self>,
58    prompt_router: PromptRouter<Self>,
59    cache_tools: CacheTools,
60    docs_tools: DocsTools,
61    deps_tools: DepsTools,
62    analysis_tools: AnalysisTools,
63    search_tools: SearchTools,
64}
65
66#[tool_router]
67impl RustDocsService {
68    pub fn new(cache_dir: Option<PathBuf>) -> Result<Self> {
69        let cache = Arc::new(RwLock::new(CrateCache::new(cache_dir)?));
70
71        Ok(Self {
72            tool_router: Self::tool_router(),
73            prompt_router: Self::prompt_router(),
74            cache_tools: CacheTools::new(cache.clone()),
75            docs_tools: DocsTools::new(cache.clone()),
76            deps_tools: DepsTools::new(cache.clone()),
77            analysis_tools: AnalysisTools::new(cache.clone()),
78            search_tools: SearchTools::new(cache),
79        })
80    }
81
82    // Cache tools
83    #[tool(
84        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."
85    )]
86    pub async fn cache_crate_from_cratesio(
87        &self,
88        Parameters(params): Parameters<CacheCrateFromCratesIOParams>,
89    ) -> String {
90        let output = self.cache_tools.cache_crate_from_cratesio(params).await;
91        output.to_json()
92    }
93
94    #[tool(
95        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."
96    )]
97    pub async fn cache_crate_from_github(
98        &self,
99        Parameters(params): Parameters<CacheCrateFromGitHubParams>,
100    ) -> String {
101        let output = self.cache_tools.cache_crate_from_github(params).await;
102        output.to_json()
103    }
104
105    #[tool(
106        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."
107    )]
108    pub async fn cache_crate_from_local(
109        &self,
110        Parameters(params): Parameters<CacheCrateFromLocalParams>,
111    ) -> String {
112        let output = self.cache_tools.cache_crate_from_local(params).await;
113        output.to_json()
114    }
115
116    #[tool(
117        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."
118    )]
119    pub async fn remove_crate(&self, Parameters(params): Parameters<RemoveCrateParams>) -> String {
120        match self.cache_tools.remove_crate(params).await {
121            Ok(output) => output.to_json(),
122            Err(error) => error.to_json(),
123        }
124    }
125
126    #[tool(
127        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."
128    )]
129    pub async fn list_cached_crates(&self) -> String {
130        match self.cache_tools.list_cached_crates().await {
131            Ok(output) => output.to_json(),
132            Err(error) => error.to_json(),
133        }
134    }
135
136    #[tool(
137        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."
138    )]
139    pub async fn list_crate_versions(
140        &self,
141        Parameters(params): Parameters<ListCrateVersionsParams>,
142    ) -> String {
143        match self.cache_tools.list_crate_versions(params).await {
144            Ok(output) => output.to_json(),
145            Err(error) => error.to_json(),
146        }
147    }
148
149    #[tool(
150        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."
151    )]
152    pub async fn get_crates_metadata(
153        &self,
154        Parameters(params): Parameters<GetCratesMetadataParams>,
155    ) -> String {
156        let output = self.cache_tools.get_crates_metadata(params).await;
157        output.to_json()
158    }
159
160    // Docs tools
161    #[tool(
162        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')."
163    )]
164    pub async fn list_crate_items(
165        &self,
166        Parameters(params): Parameters<ListItemsParams>,
167    ) -> String {
168        match self.docs_tools.list_crate_items(params).await {
169            Ok(output) => output.to_json(),
170            Err(error) => error.to_json(),
171        }
172    }
173
174    #[tool(
175        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')."
176    )]
177    pub async fn search_items(&self, Parameters(params): Parameters<SearchItemsParams>) -> String {
178        match self.docs_tools.search_items(params).await {
179            Ok(output) => output.to_json(),
180            Err(error) => error.to_json(),
181        }
182    }
183
184    #[tool(
185        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')."
186    )]
187    pub async fn search_items_preview(
188        &self,
189        Parameters(params): Parameters<SearchItemsPreviewParams>,
190    ) -> String {
191        match self.docs_tools.search_items_preview(params).await {
192            Ok(output) => output.to_json(),
193            Err(error) => error.to_json(),
194        }
195    }
196
197    #[tool(
198        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')."
199    )]
200    pub async fn get_item_details(
201        &self,
202        Parameters(params): Parameters<GetItemDetailsParams>,
203    ) -> String {
204        self.docs_tools.get_item_details(params).await.to_json()
205    }
206
207    #[tool(
208        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')."
209    )]
210    pub async fn get_item_docs(&self, Parameters(params): Parameters<GetItemDocsParams>) -> String {
211        match self.docs_tools.get_item_docs(params).await {
212            Ok(output) => output.to_json(),
213            Err(error) => error.to_json(),
214        }
215    }
216
217    #[tool(
218        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')."
219    )]
220    pub async fn get_item_source(
221        &self,
222        Parameters(params): Parameters<GetItemSourceParams>,
223    ) -> String {
224        self.docs_tools.get_item_source(params).await.to_json()
225    }
226
227    // Deps tools
228    #[tool(
229        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')."
230    )]
231    pub async fn get_dependencies(
232        &self,
233        Parameters(params): Parameters<GetDependenciesParams>,
234    ) -> String {
235        match self.deps_tools.get_dependencies(params).await {
236            Ok(output) => output.to_json(),
237            Err(error) => error.to_json(),
238        }
239    }
240
241    // Analysis tools
242    #[tool(
243        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."
244    )]
245    pub async fn structure(
246        &self,
247        Parameters(params): Parameters<AnalyzeCrateStructureParams>,
248    ) -> String {
249        match self.analysis_tools.structure(params).await {
250            Ok(output) => output.to_json(),
251            Err(error) => error.to_json(),
252        }
253    }
254
255    // Search tools
256    #[tool(
257        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')."
258    )]
259    pub async fn search_items_fuzzy(
260        &self,
261        Parameters(params): Parameters<SearchItemsFuzzyParams>,
262    ) -> String {
263        match self.search_tools.search_items_fuzzy(params).await {
264            Ok(output) => output.to_json(),
265            Err(error) => error.to_json(),
266        }
267    }
268}
269
270#[prompt_router]
271impl RustDocsService {
272    #[prompt(
273        name = "cache_dependencies",
274        description = "Cache all dependencies from a Rust project's Cargo.toml"
275    )]
276    pub async fn cache_dependencies(
277        &self,
278        Parameters(args): Parameters<CacheDependenciesArgs>,
279        _ctx: RequestContext<RoleServer>,
280    ) -> Result<Vec<PromptMessage>, ErrorData> {
281        let messages = vec![
282            PromptMessage::new_text(
283                PromptMessageRole::User,
284                format!(
285                    "I need to cache all dependencies from the Rust project{}{}. \
286                    Please analyze the Cargo.toml file{} and cache every dependency using the rust-docs MCP caching tools.",
287                    args.project_path
288                        .as_ref()
289                        .map(|p| format!(" at {p}"))
290                        .unwrap_or_else(|| " in the current working directory".to_string()),
291                    args.member_name
292                        .as_ref()
293                        .map(|m| format!(" (focusing on member: {m})"))
294                        .unwrap_or_default(),
295                    if args.force_update {
296                        " and force update existing cached dependencies"
297                    } else {
298                        ""
299                    }
300                ),
301            ),
302            PromptMessage::new_text(
303                PromptMessageRole::Assistant,
304                format!(
305                    "I'll help you cache all dependencies from the project{}. \
306                    I'll read the Cargo.toml file{}, analyze all dependencies (including dev-dependencies), \
307                    and cache them using the appropriate rust-docs MCP tools.\n\n\
308                    First, I'll aggregate a list of all dependencies with their:\n\
309                    - Source (crates.io, GitHub, or local absolute path)\n\
310                    - Full semver version (e.g., 4.0.0 not 4.0 - if minor/patch are missing, fill with zeros)\n\n\
311                    Then I'll cache them using:\n\
312                    - For crates.io dependencies: cache_crate_from_cratesio\n\
313                    - For Git dependencies: cache_crate_from_github\n\
314                    - For local path dependencies: cache_crate_from_local\n\n\
315                    Let me start by examining the Cargo.toml file to identify all dependencies.",
316                    args.project_path
317                        .as_ref()
318                        .map(|p| format!(" at '{p}'"))
319                        .unwrap_or_else(|| " in the current working directory".to_string()),
320                    args.member_name
321                        .as_ref()
322                        .map(|m| format!(" (member: {m})"))
323                        .unwrap_or_default()
324                ),
325            ),
326        ];
327
328        Ok(messages)
329    }
330}
331
332#[tool_handler]
333#[prompt_handler]
334impl ServerHandler for RustDocsService {
335    fn get_info(&self) -> ServerInfo {
336        ServerInfo {
337            server_info: rmcp::model::Implementation {
338                name: "rust-docs-mcp".to_string(),
339                version: "0.1.0".to_string(),
340                title: None,
341                website_url: None,
342                icons: None,
343            },
344            capabilities: ServerCapabilities {
345                tools: Some(Default::default()),
346                prompts: Some(Default::default()),
347                ..Default::default()
348            },
349            instructions: Some(
350                "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(),
351            ),
352            ..Default::default()
353        }
354    }
355}