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