Skip to main content

rust_analyzer_mcp/mcp/
server.rs

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};
11
12pub struct RustAnalyzerMCPServer {
13    pub(super) client: Option<RustAnalyzerClient>,
14    pub(super) workspace_root: PathBuf,
15}
16
17impl Default for RustAnalyzerMCPServer {
18    fn default() -> Self {
19        Self::new()
20    }
21}
22
23impl RustAnalyzerMCPServer {
24    pub fn new() -> Self {
25        Self {
26            client: None,
27            workspace_root: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
28        }
29    }
30
31    pub fn with_workspace(workspace_root: PathBuf) -> Self {
32        // Ensure the workspace root is absolute.
33        let workspace_root = workspace_root.canonicalize().unwrap_or_else(|_| {
34            // If canonicalize fails, try to make it absolute.
35            if workspace_root.is_absolute() {
36                workspace_root.clone()
37            } else {
38                std::env::current_dir()
39                    .unwrap_or_else(|_| PathBuf::from("."))
40                    .join(&workspace_root)
41            }
42        });
43
44        Self {
45            client: None,
46            workspace_root,
47        }
48    }
49
50    pub(super) async fn ensure_client_started(&mut self) -> Result<()> {
51        // rust-analyzer does die on occasion (it panics on some requests, see open_document());
52        // keeping a dead client around would fail every tool call for the rest of this server's
53        // life, so respawn it instead.
54        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 = RustAnalyzerClient::new(self.workspace_root.clone());
66            client.start().await?;
67            self.client = Some(client);
68        }
69        Ok(())
70    }
71
72    pub(super) async fn open_document_if_needed(&mut self, file_path: &str) -> Result<String> {
73        let absolute_path = self.workspace_root.join(file_path);
74        // Ensure we have an absolute path for the URI.
75        let absolute_path = absolute_path
76            .canonicalize()
77            .unwrap_or_else(|_| absolute_path.clone());
78        let uri = format!("file://{}", absolute_path.display());
79        let content = tokio::fs::read_to_string(&absolute_path)
80            .await
81            .map_err(|e| anyhow::anyhow!("Failed to read file {}: {}", file_path, e))?;
82
83        let Some(client) = &mut self.client else {
84            return Err(anyhow::anyhow!("Client not initialized"));
85        };
86
87        client.open_document(&uri, &content).await?;
88        Ok(uri)
89    }
90
91    /// Runs the server until its stdin reaches EOF or a shutdown signal arrives.
92    ///
93    /// Installs process-wide signal handlers that remain in effect after this returns. Reads
94    /// stdin through [`tokio::io::stdin`], whose parked blocking read cannot be cancelled: after
95    /// a signal-triggered exit the caller must not wait for the runtime to shut down on its own.
96    /// See this crate's `main.rs`, which uses [`tokio::runtime::Runtime::shutdown_background`].
97    pub async fn run(&mut self) -> Result<()> {
98        info!("Starting rust-analyzer MCP server");
99
100        let stdin = tokio::io::stdin();
101        let stdout = tokio::io::stdout();
102        let mut reader = BufReader::new(stdin);
103        let mut writer = BufWriter::new(stdout);
104
105        // Created once, up front: the streams buffer signals delivered while a request is being
106        // handled, and installing a handler permanently replaces the default disposition, so
107        // every signal must be consumed here to have an effect.
108        let mut shutdown = ShutdownSignal::new()?;
109        // How many shutdown signals were consumed; the second one escalates the cleanup below.
110        let mut signals_seen = 0u32;
111        // The first fatal I/O error, reported only after the cleanup ran.
112        let mut result = Ok(());
113
114        loop {
115            let mut line = String::new();
116            // read_line() is not cancellation-safe, but the partially read line is only lost when
117            // we shut down and discard it anyway.
118            let bytes_read = tokio::select! {
119                // Biased with the signal arm first: a signal that latched while a request was
120                // being handled must win over lines already buffered on stdin, so that no new
121                // request is accepted after shutdown was requested.
122                biased;
123                _ = shutdown.recv() => {
124                    info!("Received shutdown signal");
125                    signals_seen += 1;
126                    break;
127                }
128                read = reader.read_line(&mut line) => match read {
129                    Ok(n) => n,
130                    Err(e) => {
131                        error!("Error reading from stdin: {}", e);
132                        result = Err(e.into());
133                        break;
134                    }
135                },
136            };
137
138            if bytes_read == 0 {
139                break; // EOF
140            }
141
142            let line = line.trim();
143            if line.is_empty() {
144                continue;
145            }
146
147            let Ok(request) = serde_json::from_str::<MCPRequest>(line) else {
148                debug!("Failed to parse request: {}", line);
149                continue;
150            };
151
152            debug!("Received request: {}", request.method);
153            // A shutdown signal must not wait for the request to finish: a tool call that
154            // cold-starts rust-analyzer can run for minutes.
155            let response = tokio::select! {
156                biased;
157                _ = shutdown.recv() => {
158                    info!("Received shutdown signal");
159                    signals_seen += 1;
160                    break;
161                }
162                response = self.handle_request(request) => response,
163            };
164            // Break on errors instead of returning so rust-analyzer still gets cleaned up.
165            let response_json = match serde_json::to_string(&response) {
166                Ok(json) => json,
167                Err(e) => {
168                    error!("Failed to serialize response: {}", e);
169                    result = Err(e.into());
170                    break;
171                }
172            };
173            // Also raced against the signals: if the host stops reading stdout, a response that
174            // fills the pipe would otherwise block here forever with the signals unpolled.
175            let written = async {
176                writer.write_all(response_json.as_bytes()).await?;
177                writer.write_all(b"\n").await?;
178                writer.flush().await
179            };
180            let written = tokio::select! {
181                biased;
182                _ = shutdown.recv() => {
183                    info!("Received shutdown signal");
184                    signals_seen += 1;
185                    break;
186                }
187                written = written => written,
188            };
189            if let Err(e) = written {
190                error!("Error writing to stdout: {}", e);
191                result = Err(e.into());
192                break;
193            }
194        }
195
196        // Cleanup. client.shutdown() bounds its own graceful handshake and always ends up
197        // killing the process, so this cannot stall. A second signal — counting the one that may
198        // have triggered the exit — skips the handshake and kills rust-analyzer immediately.
199        info!("Shutting down");
200        if let Some(client) = &mut self.client {
201            let graceful = {
202                let shutting_down = client.shutdown();
203                tokio::pin!(shutting_down);
204                loop {
205                    tokio::select! {
206                        biased;
207                        _ = shutdown.recv() => {
208                            signals_seen += 1;
209                            if signals_seen >= 2 {
210                                info!("Received another shutdown signal, killing rust-analyzer");
211                                break false;
212                            }
213                        }
214                        res = &mut shutting_down => {
215                            let _ = res;
216                            break true;
217                        }
218                    }
219                }
220            };
221            if !graceful {
222                client.force_kill().await;
223            }
224        }
225
226        result
227    }
228
229    async fn handle_request(&mut self, request: MCPRequest) -> MCPResponse {
230        match request.method.as_str() {
231            "initialize" => MCPResponse::Success {
232                jsonrpc: "2.0".to_string(),
233                id: request.id,
234                result: json!({
235                    "protocolVersion": "2024-11-05",
236                    "serverInfo": {
237                        "name": "rust-analyzer-mcp",
238                        "version": env!("CARGO_PKG_VERSION")
239                    },
240                    "capabilities": {
241                        "tools": {}
242                    }
243                }),
244            },
245            "tools/list" => MCPResponse::Success {
246                jsonrpc: "2.0".to_string(),
247                id: request.id,
248                result: json!({
249                    "tools": super::tools::get_tools()
250                }),
251            },
252            "tools/call" => {
253                let Some(params) = request.params else {
254                    return MCPResponse::Error {
255                        jsonrpc: "2.0".to_string(),
256                        id: request.id,
257                        error: MCPError {
258                            code: -32602,
259                            message: "Invalid params".to_string(),
260                            data: None,
261                        },
262                    };
263                };
264
265                let Some(tool_name) = params["name"].as_str() else {
266                    return MCPResponse::Error {
267                        jsonrpc: "2.0".to_string(),
268                        id: request.id,
269                        error: MCPError {
270                            code: -32602,
271                            message: "Missing tool name".to_string(),
272                            data: None,
273                        },
274                    };
275                };
276
277                let args = params
278                    .get("arguments")
279                    .cloned()
280                    .unwrap_or_else(|| json!({}));
281
282                match super::handlers::handle_tool_call(self, tool_name, args).await {
283                    Ok(result) => MCPResponse::Success {
284                        jsonrpc: "2.0".to_string(),
285                        id: request.id,
286                        result: serde_json::to_value(result).unwrap(),
287                    },
288                    Err(e) => {
289                        error!("Tool call error: {}", e);
290                        MCPResponse::Error {
291                            jsonrpc: "2.0".to_string(),
292                            id: request.id,
293                            error: MCPError {
294                                code: -1,
295                                message: e.to_string(),
296                                data: None,
297                            },
298                        }
299                    }
300                }
301            }
302            _ => MCPResponse::Error {
303                jsonrpc: "2.0".to_string(),
304                id: request.id,
305                error: MCPError {
306                    code: -32601,
307                    message: format!("Method not found: {}", request.method),
308                    data: None,
309                },
310            },
311        }
312    }
313}
314
315/// Merged stream of the signals that request server shutdown.
316///
317/// SIGINT, SIGTERM and SIGHUP on Unix; Ctrl+C and console-close events on Windows. The streams
318/// are persistent, so signals delivered while no `recv()` is pending stay latched instead of
319/// falling through to the default disposition. Note that registering SIGHUP also overrides an
320/// inherited SIG_IGN disposition (e.g. from nohup), so a hangup always shuts the server down.
321struct ShutdownSignal {
322    #[cfg(unix)]
323    sigint: tokio::signal::unix::Signal,
324    #[cfg(unix)]
325    sigterm: tokio::signal::unix::Signal,
326    #[cfg(unix)]
327    sighup: tokio::signal::unix::Signal,
328    #[cfg(windows)]
329    ctrl_c: tokio::signal::windows::CtrlC,
330    #[cfg(windows)]
331    ctrl_close: tokio::signal::windows::CtrlClose,
332}
333
334impl ShutdownSignal {
335    fn new() -> Result<Self> {
336        #[cfg(unix)]
337        {
338            use tokio::signal::unix::{signal, SignalKind};
339
340            Ok(Self {
341                sigint: signal(SignalKind::interrupt())?,
342                sigterm: signal(SignalKind::terminate())?,
343                sighup: signal(SignalKind::hangup())?,
344            })
345        }
346        #[cfg(windows)]
347        {
348            use tokio::signal::windows;
349
350            Ok(Self {
351                ctrl_c: windows::ctrl_c()?,
352                ctrl_close: windows::ctrl_close()?,
353            })
354        }
355        #[cfg(not(any(unix, windows)))]
356        Ok(Self {})
357    }
358
359    /// Completes when the next shutdown signal arrives. Cancellation-safe.
360    async fn recv(&mut self) {
361        #[cfg(unix)]
362        {
363            tokio::select! {
364                _ = self.sigint.recv() => {}
365                _ = self.sigterm.recv() => {}
366                _ = self.sighup.recv() => {}
367            }
368        }
369        #[cfg(windows)]
370        {
371            tokio::select! {
372                _ = self.ctrl_c.recv() => {}
373                _ = self.ctrl_close.recv() => {}
374            }
375        }
376        #[cfg(not(any(unix, windows)))]
377        {
378            // No signal support; only a stdin EOF stops the server.
379            std::future::pending::<()>().await;
380        }
381    }
382}