swarm repositories / source
summaryrefslogtreecommitdiff
path: root/crates/ra-mcp-engine/src/bin
diff options
context:
space:
mode:
authormain <main@swarm.moe>2026-03-19 15:49:41 -0400
committermain <main@swarm.moe>2026-03-19 15:49:41 -0400
commitfa1bd32800b65aab31ea732dd240261b4047522c (patch)
tree2fd08af6f36b8beb3c7c941990becc1a0a091d62 /crates/ra-mcp-engine/src/bin
downloadadequate-rust-mcp-1.0.0.zip
Release adequate-rust-mcp 1.0.0v1.0.0
Diffstat (limited to 'crates/ra-mcp-engine/src/bin')
-rw-r--r--crates/ra-mcp-engine/src/bin/fake-rust-analyzer.rs467
1 files changed, 467 insertions, 0 deletions
diff --git a/crates/ra-mcp-engine/src/bin/fake-rust-analyzer.rs b/crates/ra-mcp-engine/src/bin/fake-rust-analyzer.rs
new file mode 100644
index 0000000..c64b68b
--- /dev/null
+++ b/crates/ra-mcp-engine/src/bin/fake-rust-analyzer.rs
@@ -0,0 +1,467 @@
+//! Fault-injectable fake rust-analyzer used by integration tests.
+
+use lsp_types as _;
+use ra_mcp_domain as _;
+use ra_mcp_engine as _;
+use serde as _;
+use serde_json::{Value, json};
+#[cfg(test)]
+use serial_test as _;
+use std::{
+ fs,
+ io::{self, BufRead, BufReader, Read, Write},
+ path::{Path, PathBuf},
+ time::Duration,
+};
+#[cfg(test)]
+use tempfile as _;
+use thiserror as _;
+use tokio as _;
+use tracing as _;
+use url as _;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum Mode {
+ Stable,
+ CrashOnFirstHover,
+}
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ run().map_err(|error| Box::new(error) as Box<dyn std::error::Error>)
+}
+
+fn run() -> io::Result<()> {
+ let mut mode = Mode::Stable;
+ let mut marker = None::<PathBuf>;
+ let mut hover_delay = Duration::ZERO;
+ let mut execute_command_delay = Duration::ZERO;
+ let mut execute_command_log = None::<PathBuf>;
+ let mut diagnostic_warmup_count = 0_u8;
+ let mut diagnostic_cancel_count = 0_u8;
+ let mut strict_root_match = false;
+ let mut workspace_root = None::<PathBuf>;
+ let mut args = std::env::args().skip(1);
+ loop {
+ let argument = args.next();
+ let Some(argument) = argument else {
+ break;
+ };
+ match argument.as_str() {
+ "--mode" => {
+ if let Some(value) = args.next() {
+ mode = parse_mode(&value).unwrap_or(Mode::Stable);
+ }
+ }
+ "--crash-marker" => {
+ if let Some(value) = args.next() {
+ marker = Some(PathBuf::from(value));
+ }
+ }
+ "--hover-delay-ms" => {
+ if let Some(value) = args.next() {
+ let parsed = value.parse::<u64>().ok();
+ if let Some(delay_ms) = parsed {
+ hover_delay = Duration::from_millis(delay_ms);
+ }
+ }
+ }
+ "--execute-command-delay-ms" => {
+ if let Some(value) = args.next() {
+ let parsed = value.parse::<u64>().ok();
+ if let Some(delay_ms) = parsed {
+ execute_command_delay = Duration::from_millis(delay_ms);
+ }
+ }
+ }
+ "--execute-command-log" => {
+ if let Some(value) = args.next() {
+ execute_command_log = Some(PathBuf::from(value));
+ }
+ }
+ "--diagnostic-warmup-count" => {
+ if let Some(value) = args.next() {
+ let parsed = value.parse::<u8>().ok();
+ if let Some(count) = parsed {
+ diagnostic_warmup_count = count;
+ }
+ }
+ }
+ "--diagnostic-cancel-count" => {
+ if let Some(value) = args.next() {
+ let parsed = value.parse::<u8>().ok();
+ if let Some(count) = parsed {
+ diagnostic_cancel_count = count;
+ }
+ }
+ }
+ "--strict-root-match" => {
+ strict_root_match = true;
+ }
+ _ => {}
+ }
+ }
+
+ let stdin = io::stdin();
+ let stdout = io::stdout();
+ let mut reader = BufReader::new(stdin.lock());
+ let mut writer = stdout.lock();
+
+ loop {
+ let frame = match read_frame(&mut reader) {
+ Ok(frame) => frame,
+ Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => break,
+ Err(error) => return Err(error),
+ };
+ let message: Value = serde_json::from_slice(&frame)
+ .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error.to_string()))?;
+ if let Some(method) = message.get("method").and_then(Value::as_str) {
+ if method == "initialized" {
+ continue;
+ }
+
+ let request_id = message.get("id").cloned();
+ let Some(request_id) = request_id else {
+ continue;
+ };
+ if method == "initialize" {
+ workspace_root = initialized_workspace_root(&message);
+ }
+
+ if mode == Mode::CrashOnFirstHover
+ && method == "textDocument/hover"
+ && should_crash(&marker)?
+ {
+ std::process::exit(0);
+ }
+ if method == "textDocument/hover" && !hover_delay.is_zero() {
+ std::thread::sleep(hover_delay);
+ }
+ if method == "workspace/executeCommand" {
+ if let Some(path) = execute_command_log.as_ref() {
+ log_execute_command_effect(path, &message)?;
+ }
+ if !execute_command_delay.is_zero() {
+ std::thread::sleep(execute_command_delay);
+ }
+ }
+
+ let response = if strict_root_match
+ && request_targets_outside_workspace(&message, workspace_root.as_deref())
+ {
+ strict_root_mismatch_response(method, request_id, &message)
+ } else if method == "textDocument/diagnostic" && diagnostic_cancel_count > 0 {
+ diagnostic_cancel_count = diagnostic_cancel_count.saturating_sub(1);
+ server_cancelled_response(request_id)
+ } else if method == "textDocument/diagnostic" && diagnostic_warmup_count > 0 {
+ diagnostic_warmup_count = diagnostic_warmup_count.saturating_sub(1);
+ warmup_unlinked_diagnostic_response(request_id)
+ } else {
+ make_response(method, request_id, &message)
+ };
+ write_frame(&mut writer, &response)?;
+ }
+ }
+
+ Ok(())
+}
+
+fn parse_mode(raw: &str) -> Option<Mode> {
+ match raw {
+ "stable" => Some(Mode::Stable),
+ "crash_on_first_hover" => Some(Mode::CrashOnFirstHover),
+ _ => None,
+ }
+}
+
+fn should_crash(marker: &Option<PathBuf>) -> io::Result<bool> {
+ let Some(marker) = marker else {
+ return Ok(true);
+ };
+ if marker.exists() {
+ return Ok(false);
+ }
+ fs::write(marker, b"crashed")?;
+ Ok(true)
+}
+
+fn log_execute_command_effect(path: &PathBuf, request: &Value) -> io::Result<()> {
+ let command = request
+ .get("params")
+ .and_then(|params| params.get("command"))
+ .and_then(Value::as_str)
+ .unwrap_or("<missing-command>");
+ let mut file = fs::OpenOptions::new()
+ .create(true)
+ .append(true)
+ .open(path)?;
+ writeln!(file, "{command}")?;
+ Ok(())
+}
+
+fn initialized_workspace_root(request: &Value) -> Option<PathBuf> {
+ let root_uri = request
+ .get("params")
+ .and_then(|params| params.get("rootUri"))
+ .and_then(Value::as_str)?;
+ let root_url = url::Url::parse(root_uri).ok()?;
+ root_url.to_file_path().ok()
+}
+
+fn request_targets_outside_workspace(request: &Value, workspace_root: Option<&Path>) -> bool {
+ let Some(workspace_root) = workspace_root else {
+ return false;
+ };
+ let file_path = request_document_path(request);
+ let Some(file_path) = file_path else {
+ return false;
+ };
+ !file_path.starts_with(workspace_root)
+}
+
+fn request_document_path(request: &Value) -> Option<PathBuf> {
+ let uri = request
+ .get("params")
+ .and_then(|params| params.get("textDocument"))
+ .and_then(|doc| doc.get("uri"))
+ .and_then(Value::as_str)?;
+ let url = url::Url::parse(uri).ok()?;
+ url.to_file_path().ok()
+}
+
+fn strict_root_mismatch_response(method: &str, request_id: Value, request: &Value) -> Value {
+ match method {
+ "textDocument/hover" => json!({
+ "jsonrpc": "2.0",
+ "id": request_id,
+ "result": Value::Null
+ }),
+ "textDocument/definition" => json!({
+ "jsonrpc": "2.0",
+ "id": request_id,
+ "result": Value::Null
+ }),
+ "textDocument/references" => json!({
+ "jsonrpc": "2.0",
+ "id": request_id,
+ "result": Value::Null
+ }),
+ "textDocument/rename" => {
+ let uri = request
+ .get("params")
+ .and_then(|params| params.get("textDocument"))
+ .and_then(|doc| doc.get("uri"))
+ .and_then(Value::as_str)
+ .unwrap_or("file:///tmp/fallback.rs")
+ .to_owned();
+ json!({
+ "jsonrpc": "2.0",
+ "id": request_id,
+ "result": {
+ "changes": {
+ uri: []
+ }
+ }
+ })
+ }
+ "textDocument/diagnostic" => warmup_unlinked_diagnostic_response(request_id),
+ _ => make_response(method, request_id, request),
+ }
+}
+
+fn make_response(method: &str, request_id: Value, request: &Value) -> Value {
+ match method {
+ "initialize" => json!({
+ "jsonrpc": "2.0",
+ "id": request_id,
+ "result": {
+ "capabilities": {}
+ }
+ }),
+ "textDocument/hover" => json!({
+ "jsonrpc": "2.0",
+ "id": request_id,
+ "result": {
+ "contents": {
+ "kind": "markdown",
+ "value": "hover::ok"
+ }
+ }
+ }),
+ "textDocument/definition" => {
+ let uri = request
+ .get("params")
+ .and_then(|params| params.get("textDocument"))
+ .and_then(|doc| doc.get("uri"))
+ .cloned()
+ .unwrap_or(Value::String("file:///tmp/fallback.rs".to_owned()));
+ json!({
+ "jsonrpc": "2.0",
+ "id": request_id,
+ "result": [{
+ "uri": uri,
+ "range": {
+ "start": { "line": 2, "character": 3 },
+ "end": { "line": 2, "character": 8 }
+ }
+ }]
+ })
+ }
+ "textDocument/references" => {
+ let uri = request
+ .get("params")
+ .and_then(|params| params.get("textDocument"))
+ .and_then(|doc| doc.get("uri"))
+ .cloned()
+ .unwrap_or(Value::String("file:///tmp/fallback.rs".to_owned()));
+ json!({
+ "jsonrpc": "2.0",
+ "id": request_id,
+ "result": [{
+ "uri": uri,
+ "range": {
+ "start": { "line": 4, "character": 1 },
+ "end": { "line": 4, "character": 5 }
+ }
+ }]
+ })
+ }
+ "textDocument/rename" => {
+ let uri = request
+ .get("params")
+ .and_then(|params| params.get("textDocument"))
+ .and_then(|doc| doc.get("uri"))
+ .and_then(Value::as_str)
+ .unwrap_or("file:///tmp/fallback.rs")
+ .to_owned();
+ json!({
+ "jsonrpc": "2.0",
+ "id": request_id,
+ "result": {
+ "changes": {
+ uri: [
+ {
+ "range": {
+ "start": { "line": 1, "character": 1 },
+ "end": { "line": 1, "character": 4 }
+ },
+ "newText": "renamed_symbol"
+ }
+ ]
+ }
+ }
+ })
+ }
+ "textDocument/diagnostic" => json!({
+ "jsonrpc": "2.0",
+ "id": request_id,
+ "result": {
+ "kind": "full",
+ "items": [{
+ "range": {
+ "start": { "line": 0, "character": 0 },
+ "end": { "line": 0, "character": 3 }
+ },
+ "severity": 1,
+ "message": "fake diagnostic"
+ }]
+ }
+ }),
+ "workspace/executeCommand" => {
+ let command = request
+ .get("params")
+ .and_then(|params| params.get("command"))
+ .cloned()
+ .unwrap_or(Value::Null);
+ json!({
+ "jsonrpc": "2.0",
+ "id": request_id,
+ "result": {
+ "ack": "ok",
+ "command": command
+ }
+ })
+ }
+ _ => json!({
+ "jsonrpc": "2.0",
+ "id": request_id,
+ "error": {
+ "code": -32601,
+ "message": format!("method not found: {method}")
+ }
+ }),
+ }
+}
+
+fn warmup_unlinked_diagnostic_response(request_id: Value) -> Value {
+ json!({
+ "jsonrpc": "2.0",
+ "id": request_id,
+ "result": {
+ "kind": "full",
+ "items": [{
+ "range": {
+ "start": { "line": 0, "character": 0 },
+ "end": { "line": 0, "character": 0 }
+ },
+ "severity": 2,
+ "code": "unlinked-file",
+ "message": "This file is not part of any crate, so rust-analyzer can't offer IDE services."
+ }]
+ }
+ })
+}
+
+fn server_cancelled_response(request_id: Value) -> Value {
+ json!({
+ "jsonrpc": "2.0",
+ "id": request_id,
+ "error": {
+ "code": -32802,
+ "message": "server cancelled request during workspace reload"
+ }
+ })
+}
+
+fn read_frame(reader: &mut BufReader<impl Read>) -> io::Result<Vec<u8>> {
+ let mut content_length = None::<usize>;
+ loop {
+ let mut line = String::new();
+ let bytes = reader.read_line(&mut line)?;
+ if bytes == 0 {
+ return Err(io::Error::new(
+ io::ErrorKind::UnexpectedEof,
+ "EOF while reading headers",
+ ));
+ }
+ if line == "\r\n" || line == "\n" {
+ break;
+ }
+ let trimmed = line.trim_end_matches(['\r', '\n']);
+ if let Some(raw_length) = trimmed.strip_prefix("Content-Length:") {
+ let parsed = raw_length.trim().parse::<usize>().map_err(|error| {
+ io::Error::new(
+ io::ErrorKind::InvalidData,
+ format!("invalid Content-Length header: {error}"),
+ )
+ })?;
+ content_length = Some(parsed);
+ }
+ }
+
+ let length = content_length.ok_or_else(|| {
+ io::Error::new(io::ErrorKind::InvalidData, "missing Content-Length header")
+ })?;
+ let mut payload = vec![0_u8; length];
+ reader.read_exact(&mut payload)?;
+ Ok(payload)
+}
+
+fn write_frame(writer: &mut impl Write, payload: &Value) -> io::Result<()> {
+ let serialized = serde_json::to_vec(payload)
+ .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error.to_string()))?;
+ let header = format!("Content-Length: {}\r\n\r\n", serialized.len());
+ writer.write_all(header.as_bytes())?;
+ writer.write_all(&serialized)?;
+ writer.flush()?;
+ Ok(())
+}