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 #[serde(skip_serializing_if = "Option::is_none")]
42 pub project_path: Option<String>,
43
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub member_name: Option<String>,
47
48 #[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 #[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 #[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 #[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 #[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 #[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}