1use anyhow::Result;
2use log::{debug, error, info, warn};
3use serde_json::json;
4use std::path::PathBuf;
5use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter};
6
7use crate::{
8 lsp::RustAnalyzerClient,
9 protocol::mcp::{MCPError, MCPRequest, MCPResponse},
10 settings::Settings,
11 uri,
12};
13
14pub struct RustAnalyzerMCPServer {
15 pub(super) client: Option<RustAnalyzerClient>,
16 pub(super) workspace_root: PathBuf,
17 pub(super) settings: Settings,
19}
20
21impl Default for RustAnalyzerMCPServer {
22 fn default() -> Self {
23 Self::new()
24 }
25}
26
27impl RustAnalyzerMCPServer {
28 pub fn new() -> Self {
29 Self {
30 client: None,
31 workspace_root: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
32 settings: Settings::default(),
33 }
34 }
35
36 pub fn with_workspace(workspace_root: PathBuf) -> Self {
37 Self {
38 client: None,
39 workspace_root: uri::absolute(&workspace_root),
40 settings: Settings::default(),
41 }
42 }
43
44 pub fn with_settings(mut self, settings: Settings) -> Self {
46 self.settings = settings;
47 self
48 }
49
50 pub(super) async fn ensure_client_started(&mut self) -> Result<()> {
51 if let Some(client) = &mut self.client {
55 if client.is_gone() {
56 match client.exit_status() {
57 Some(status) => warn!("rust-analyzer exited ({status}), restarting it"),
58 None => warn!("rust-analyzer closed its connection, restarting it"),
59 }
60 self.client = None;
61 }
62 }
63
64 if self.client.is_none() {
65 let mut client =
66 RustAnalyzerClient::new(self.workspace_root.clone(), self.settings.to_json());
67 client.start().await?;
68 self.client = Some(client);
69 }
70 Ok(())
71 }
72
73 pub(super) async fn open_document_if_needed(&mut self, file_path: &str) -> Result<String> {
74 let path = self.resolve_path(file_path);
75 let uri = uri::path_to_uri(&path)?;
76 let content = tokio::fs::read_to_string(&path)
77 .await
78 .map_err(|e| anyhow::anyhow!("Failed to read file {}: {}", path.display(), e))?;
79
80 let Some(client) = &mut self.client else {
81 return Err(anyhow::anyhow!("Client not initialized"));
82 };
83
84 client.open_document(&uri, &content).await?;
85 Ok(uri)
86 }
87
88 pub(super) async fn refresh_open_documents(&mut self) -> Result<()> {
94 let Some(client) = &mut self.client else {
95 return Err(anyhow::anyhow!("Client not initialized"));
96 };
97
98 for uri in client.open_document_uris().await {
99 let Some(path) = uri::uri_to_path(&uri) else {
100 continue;
101 };
102
103 match tokio::fs::read_to_string(&path).await {
104 Ok(content) => client.open_document(&uri, &content).await?,
105 Err(_) => client.close_document(&uri).await?,
108 }
109 }
110
111 Ok(())
112 }
113
114 pub(super) fn resolve_path(&self, file_path: &str) -> PathBuf {
119 let path = match uri::uri_to_path(file_path) {
120 Some(path) => path,
121 None => self.workspace_root.join(file_path),
123 };
124
125 uri::absolute(&path)
126 }
127
128 pub async fn run(&mut self) -> Result<()> {
135 info!("Starting rust-analyzer MCP server");
136
137 let stdin = tokio::io::stdin();
138 let stdout = tokio::io::stdout();
139 let mut reader = BufReader::new(stdin);
140 let mut writer = BufWriter::new(stdout);
141
142 let mut shutdown = ShutdownSignal::new()?;
146 let mut signals_seen = 0u32;
148 let mut result = Ok(());
150
151 loop {
152 let mut line = String::new();
153 let bytes_read = tokio::select! {
156 biased;
160 _ = shutdown.recv() => {
161 info!("Received shutdown signal");
162 signals_seen += 1;
163 break;
164 }
165 read = reader.read_line(&mut line) => match read {
166 Ok(n) => n,
167 Err(e) => {
168 error!("Error reading from stdin: {}", e);
169 result = Err(e.into());
170 break;
171 }
172 },
173 };
174
175 if bytes_read == 0 {
176 break; }
178
179 let line = line.trim();
180 if line.is_empty() {
181 continue;
182 }
183
184 let Ok(request) = serde_json::from_str::<MCPRequest>(line) else {
185 debug!("Failed to parse request: {}", line);
186 continue;
187 };
188
189 if request.id.is_none() {
194 debug!("Ignoring notification: {}", request.method);
195 continue;
196 }
197
198 debug!("Received request: {}", request.method);
199 let response = tokio::select! {
202 biased;
203 _ = shutdown.recv() => {
204 info!("Received shutdown signal");
205 signals_seen += 1;
206 break;
207 }
208 response = self.handle_request(request) => response,
209 };
210 let response_json = match serde_json::to_string(&response) {
212 Ok(json) => json,
213 Err(e) => {
214 error!("Failed to serialize response: {}", e);
215 result = Err(e.into());
216 break;
217 }
218 };
219 let written = async {
222 writer.write_all(response_json.as_bytes()).await?;
223 writer.write_all(b"\n").await?;
224 writer.flush().await
225 };
226 let written = tokio::select! {
227 biased;
228 _ = shutdown.recv() => {
229 info!("Received shutdown signal");
230 signals_seen += 1;
231 break;
232 }
233 written = written => written,
234 };
235 if let Err(e) = written {
236 error!("Error writing to stdout: {}", e);
237 result = Err(e.into());
238 break;
239 }
240 }
241
242 info!("Shutting down");
246 if let Some(client) = &mut self.client {
247 let graceful = {
248 let shutting_down = client.shutdown();
249 tokio::pin!(shutting_down);
250 loop {
251 tokio::select! {
252 biased;
253 _ = shutdown.recv() => {
254 signals_seen += 1;
255 if signals_seen >= 2 {
256 info!("Received another shutdown signal, killing rust-analyzer");
257 break false;
258 }
259 }
260 res = &mut shutting_down => {
261 let _ = res;
262 break true;
263 }
264 }
265 }
266 };
267 if !graceful {
268 client.force_kill().await;
269 }
270 }
271
272 result
273 }
274
275 async fn handle_request(&mut self, request: MCPRequest) -> MCPResponse {
276 match request.method.as_str() {
277 "initialize" => MCPResponse::Success {
278 jsonrpc: "2.0".to_string(),
279 id: request.id,
280 result: json!({
281 "protocolVersion": "2024-11-05",
282 "serverInfo": {
283 "name": "rust-analyzer-mcp",
284 "version": env!("CARGO_PKG_VERSION")
285 },
286 "capabilities": {
287 "tools": {}
288 }
289 }),
290 },
291 "ping" => MCPResponse::Success {
294 jsonrpc: "2.0".to_string(),
295 id: request.id,
296 result: json!({}),
297 },
298 "tools/list" => MCPResponse::Success {
299 jsonrpc: "2.0".to_string(),
300 id: request.id,
301 result: json!({
302 "tools": super::tools::get_tools()
303 }),
304 },
305 "tools/call" => {
306 let Some(params) = request.params else {
307 return MCPResponse::Error {
308 jsonrpc: "2.0".to_string(),
309 id: request.id,
310 error: MCPError {
311 code: -32602,
312 message: "Invalid params".to_string(),
313 data: None,
314 },
315 };
316 };
317
318 let Some(tool_name) = params["name"].as_str() else {
319 return MCPResponse::Error {
320 jsonrpc: "2.0".to_string(),
321 id: request.id,
322 error: MCPError {
323 code: -32602,
324 message: "Missing tool name".to_string(),
325 data: None,
326 },
327 };
328 };
329
330 let args = params
331 .get("arguments")
332 .cloned()
333 .unwrap_or_else(|| json!({}));
334
335 match super::handlers::handle_tool_call(self, tool_name, args).await {
336 Ok(result) => MCPResponse::Success {
337 jsonrpc: "2.0".to_string(),
338 id: request.id,
339 result: serde_json::to_value(result).unwrap(),
340 },
341 Err(e) => {
342 error!("Tool call error: {}", e);
343 MCPResponse::Error {
344 jsonrpc: "2.0".to_string(),
345 id: request.id,
346 error: MCPError {
347 code: -1,
348 message: e.to_string(),
349 data: None,
350 },
351 }
352 }
353 }
354 }
355 _ => MCPResponse::Error {
356 jsonrpc: "2.0".to_string(),
357 id: request.id,
358 error: MCPError {
359 code: -32601,
360 message: format!("Method not found: {}", request.method),
361 data: None,
362 },
363 },
364 }
365 }
366}
367
368struct ShutdownSignal {
375 #[cfg(unix)]
376 sigint: tokio::signal::unix::Signal,
377 #[cfg(unix)]
378 sigterm: tokio::signal::unix::Signal,
379 #[cfg(unix)]
380 sighup: tokio::signal::unix::Signal,
381 #[cfg(windows)]
382 ctrl_c: tokio::signal::windows::CtrlC,
383 #[cfg(windows)]
384 ctrl_close: tokio::signal::windows::CtrlClose,
385}
386
387impl ShutdownSignal {
388 fn new() -> Result<Self> {
389 #[cfg(unix)]
390 {
391 use tokio::signal::unix::{signal, SignalKind};
392
393 Ok(Self {
394 sigint: signal(SignalKind::interrupt())?,
395 sigterm: signal(SignalKind::terminate())?,
396 sighup: signal(SignalKind::hangup())?,
397 })
398 }
399 #[cfg(windows)]
400 {
401 use tokio::signal::windows;
402
403 Ok(Self {
404 ctrl_c: windows::ctrl_c()?,
405 ctrl_close: windows::ctrl_close()?,
406 })
407 }
408 #[cfg(not(any(unix, windows)))]
409 Ok(Self {})
410 }
411
412 async fn recv(&mut self) {
414 #[cfg(unix)]
415 {
416 tokio::select! {
417 _ = self.sigint.recv() => {}
418 _ = self.sigterm.recv() => {}
419 _ = self.sighup.recv() => {}
420 }
421 }
422 #[cfg(windows)]
423 {
424 tokio::select! {
425 _ = self.ctrl_c.recv() => {}
426 _ = self.ctrl_close.recv() => {}
427 }
428 }
429 #[cfg(not(any(unix, windows)))]
430 {
431 std::future::pending::<()>().await;
433 }
434 }
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440
441 #[test]
442 fn file_paths_are_resolved_however_they_are_spelled() {
443 let server = RustAnalyzerMCPServer::with_workspace(workspace());
444 let absolute = server.workspace_root.join("src/lib.rs");
445 let uri = uri::path_to_uri(&absolute).unwrap();
446
447 for spelling in ["src/lib.rs", &absolute.display().to_string(), &uri] {
448 let resolved = server.resolve_path(spelling);
449
450 assert_eq!(resolved, absolute, "{spelling}");
451 assert!(std::fs::read_to_string(&resolved).is_ok(), "{spelling}");
454 }
455 }
456
457 #[test]
458 fn a_path_that_does_not_exist_still_resolves() {
459 let server = RustAnalyzerMCPServer::with_workspace(workspace());
461 let missing = server.workspace_root.join("src/nowhere.rs");
462
463 assert_eq!(server.resolve_path("src/nowhere.rs"), missing);
464 }
465
466 fn workspace() -> PathBuf {
468 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-project")
469 }
470}