finalized refactors, woot

Phillip Davis created

Change summary

Cargo.lock            | 121 +++++++++++++++++++++++++++++++++++++++++
Cargo.toml            |   2 
crush.json            |  12 ----
src/config.rs         |   2 
src/lsp/client.rs     | 132 +++++++++++++++++++++++++-------------------
src/lsp/mod.rs        |   2 
src/main.rs           |  41 +++----------
src/mcp/mod.rs        |   2 
src/mcp/server.rs     |  60 ++++++++++++++++----
src/mcp/tools/read.rs |   6 -
10 files changed, 260 insertions(+), 120 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -17,6 +17,15 @@ version = "2.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
 
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "android_system_properties"
 version = "0.1.5"
@@ -76,6 +85,12 @@ dependencies = [
  "windows-sys 0.60.2",
 ]
 
+[[package]]
+name = "anyhow"
+version = "1.0.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
+
 [[package]]
 name = "async-channel"
 version = "2.5.0"
@@ -768,6 +783,12 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
 [[package]]
 name = "libc"
 version = "0.2.176"
@@ -809,6 +830,7 @@ dependencies = [
 name = "lsp2mcp"
 version = "0.1.0"
 dependencies = [
+ "anyhow",
  "async-lsp",
  "async-process",
  "clap",
@@ -819,6 +841,16 @@ dependencies = [
  "toml",
  "tower",
  "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "matchers"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+dependencies = [
+ "regex-automata",
 ]
 
 [[package]]
@@ -847,6 +879,15 @@ dependencies = [
  "windows-sys 0.59.0",
 ]
 
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "num-traits"
 version = "0.2.19"
@@ -979,6 +1020,23 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "regex-automata"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
+
 [[package]]
 name = "rmcp"
 version = "0.8.0"
@@ -1144,6 +1202,15 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
 [[package]]
 name = "shlex"
 version = "1.3.0"
@@ -1235,6 +1302,15 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "thread_local"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+dependencies = [
+ "cfg-if",
+]
+
 [[package]]
 name = "tinystr"
 version = "0.8.1"
@@ -1379,6 +1455,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
 dependencies = [
  "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex-automata",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
 ]
 
 [[package]]
@@ -1411,6 +1517,12 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
 
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
 [[package]]
 name = "waitpid-any"
 version = "0.3.0"
@@ -1545,6 +1657,15 @@ dependencies = [
  "windows-link",
 ]
 
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
 [[package]]
 name = "windows-sys"
 version = "0.59.0"

Cargo.toml 🔗

@@ -4,6 +4,7 @@ version = "0.1.0"
 edition = "2024"
 
 [dependencies]
+anyhow = "1.0.100"
 async-lsp = "0.2.2"
 async-process = "2.3"
 clap = { version = "4.5.48", features = ["derive"] }
@@ -14,3 +15,4 @@ tokio = { version = "1.47.1", features = ["macros", "rt", "rt-multi-thread", "fs
 toml = "0.8"
 tower = "0.5.2"
 tracing = "0.1.41"
+tracing-subscriber = { version = "0.3.20", features = ["env-filter", "fmt", "std"] }

crush.json 🔗

@@ -5,17 +5,5 @@
 			"command": "rust-analyzer",
 			"args": []
 		}
-	},
-	"mcp": {
-		"lsp2mcp": {
-			"type": "stdio",
-			"command": "cargo",
-			"args": [
-				"run",
-				"--",
-				"--config",
-				"sample_config.json"
-			]
-		}
 	}
 }

src/config.rs 🔗

@@ -22,7 +22,7 @@ pub struct Config {
 }
 
 impl Config {
-    pub async fn load(path: &PathBuf) -> Result<Self, Box<dyn std::error::Error>> {
+    pub async fn load(path: &PathBuf) -> anyhow::Result<Self> {
         let config_content = tokio::fs::read_to_string(path).await?;
         let config: Config = serde_json::from_str(&config_content)?;
         Ok(config)

src/lsp/client.rs 🔗

@@ -7,8 +7,8 @@ use async_lsp::lsp_types::{
     TextDocumentClientCapabilities, Url, WindowClientCapabilities, WorkspaceFolder,
 };
 use async_lsp::router::Router;
-use async_lsp::{LanguageServer, ServerSocket as LSPServerSocket};
-use async_process::{Command as ProcessCommand, Stdio};
+use async_lsp::{LanguageServer, MainLoop, ServerSocket as LSPServerSocket};
+use async_process::{Child, Command as ProcessCommand, Stdio};
 use futures::channel::oneshot;
 use tower::ServiceBuilder;
 
@@ -20,68 +20,86 @@ mod control_flow_state {
     pub struct Stop;
 }
 
-pub async fn setup_lsp_client(
-    lsp_command: &str,
-    lsp_args: &[String],
-    project_root: &PathBuf,
-) -> Result<(tokio::task::JoinHandle<()>, LSPServerSocket), Box<dyn std::error::Error>> {
-    let mut child = ProcessCommand::new(lsp_command)
-        .args(lsp_args)
-        .current_dir(project_root)
-        .stdin(Stdio::piped())
-        .stdout(Stdio::piped())
-        .stderr(Stdio::inherit())
-        .kill_on_drop(true)
-        .spawn()
-        .expect(format!("Failed to start lsp: {} {:?}", lsp_command, lsp_args).as_ref());
+pub struct LSPClient {
+    child: Child,
+    pub mainloop: MainLoop<Router<LSPClientState>>,
+    pub server: LSPServerSocket,
+}
+
+impl LSPClient {
+    pub async fn setup(
+        lsp_command: &str,
+        lsp_args: &[String],
+        project_root: &PathBuf,
+    ) -> anyhow::Result<Self> {
+        let child = ProcessCommand::new(lsp_command)
+            .args(lsp_args)
+            .current_dir(project_root)
+            .stdin(Stdio::piped())
+            .stdout(Stdio::piped())
+            .stderr(Stdio::inherit())
+            .kill_on_drop(true)
+            .spawn()
+            .expect(format!("Failed to start lsp: {} {:?}", lsp_command, lsp_args).as_ref());
 
-    let stdin = child.stdin.take().unwrap();
-    let stdout = child.stdout.take().unwrap();
+        let (mainloop, mut server) = async_lsp::MainLoop::new_client(|_server| {
+            let mut router = Router::new(LSPClientState { indexed_tx: None });
 
-    let (mainloop, mut server) = async_lsp::MainLoop::new_client(|_server| {
-        let mut router = Router::new(LSPClientState { indexed_tx: None });
+            router
+                .notification::<Progress>(|_this, _params| ControlFlow::Continue(()))
+                .notification::<PublishDiagnostics>(|_this, _params| ControlFlow::Continue(()))
+                .notification::<ShowMessage>(|_this, _params| ControlFlow::Continue(()))
+                .event(|_, _: control_flow_state::Stop| ControlFlow::Break(Ok(())));
 
-        router
-            .notification::<Progress>(|_this, _params| ControlFlow::Continue(()))
-            .notification::<PublishDiagnostics>(|_this, _params| ControlFlow::Continue(()))
-            .notification::<ShowMessage>(|_this, _params| ControlFlow::Continue(()))
-            .event(|_, _: control_flow_state::Stop| ControlFlow::Break(Ok(())));
+            ServiceBuilder::new().service(router)
+        });
 
-        ServiceBuilder::new().service(router)
-    });
+        let project_root_canonicalized = tokio::fs::canonicalize(project_root)
+            .await
+            .expect("Failed to canonicalize project root");
 
-    let mainloop_fut = tokio::spawn(async move {
-        mainloop.run_buffered(stdout, stdin).await.unwrap();
-    });
+        let uri = Url::from_file_path(&project_root_canonicalized)
+            .expect("Failed to create URL from project_root path");
 
-    let project_root_canonicalized = tokio::fs::canonicalize(project_root)
-        .await
-        .expect("Failed to canonicalize project root");
-    let uri = Url::from_file_path(&project_root_canonicalized).unwrap();
-    let _init_ret = server
-        .initialize(InitializeParams {
-            workspace_folders: Some(vec![WorkspaceFolder {
-                uri: uri,
-                name: "root".into(),
-            }]),
-            capabilities: ClientCapabilities {
-                window: Some(WindowClientCapabilities {
-                    work_done_progress: Some(true),
-                    ..WindowClientCapabilities::default()
-                }),
-                text_document: Some(TextDocumentClientCapabilities {
-                    diagnostic: Some(DiagnosticClientCapabilities {
-                        ..DiagnosticClientCapabilities::default()
+        let _init_ret = server
+            .initialize(InitializeParams {
+                workspace_folders: Some(vec![WorkspaceFolder {
+                    uri: uri,
+                    name: "root".into(),
+                }]),
+                capabilities: ClientCapabilities {
+                    window: Some(WindowClientCapabilities {
+                        work_done_progress: Some(true),
+                        ..WindowClientCapabilities::default()
+                    }),
+                    text_document: Some(TextDocumentClientCapabilities {
+                        diagnostic: Some(DiagnosticClientCapabilities {
+                            ..DiagnosticClientCapabilities::default()
+                        }),
+                        ..TextDocumentClientCapabilities::default()
                     }),
-                    ..TextDocumentClientCapabilities::default()
-                }),
-                ..ClientCapabilities::default()
-            },
-            ..InitializeParams::default()
+                    ..ClientCapabilities::default()
+                },
+                ..InitializeParams::default()
+            })
+            .await
+            .unwrap();
+
+        server
+            .initialized(InitializedParams {})
+            .expect("Bad response to Initialized message");
+
+        Ok(Self {
+            child,
+            mainloop,
+            server,
         })
-        .await
-        .unwrap();
-    server.initialized(InitializedParams {}).unwrap();
+    }
 
-    Ok((mainloop_fut, server))
+    pub async fn run_main_loop(mut self) -> anyhow::Result<()> {
+        let stdin = self.child.stdin.take().unwrap();
+        let stdout = self.child.stdout.take().unwrap();
+        self.mainloop.run_buffered(stdout, stdin).await?;
+        Ok(())
+    }
 }

src/lsp/mod.rs 🔗

@@ -1,3 +1,3 @@
 pub mod client;
 
-pub use client::setup_lsp_client;
+pub use client::LSPClient;

src/main.rs 🔗

@@ -3,44 +3,23 @@ mod lsp;
 mod mcp;
 
 use clap::Parser;
-use futures::join;
-use rmcp::ServiceExt;
 
 use config::{CommandLineArgs, Config};
-use lsp::setup_lsp_client;
-use mcp::{MCPServer, MCPServerConfig};
+use tracing_subscriber::EnvFilter;
 
 #[tokio::main]
-async fn main() -> Result<(), Box<dyn std::error::Error>> {
+async fn main() -> anyhow::Result<()> {
+    tracing_subscriber::fmt()
+        .with_env_filter(EnvFilter::from_default_env().add_directive(tracing::Level::DEBUG.into()))
+        .with_writer(std::io::stderr)
+        .with_ansi(false)
+        .init();
+
     let args = CommandLineArgs::parse();
     let config = Config::load(&args.config).await?;
 
-    let (mainloop_fut, lsp_server) = setup_lsp_client(
-        &config.lsp_server_command.command,
-        &config.lsp_server_command.args,
-        &config.project_root,
-    )
-    .await?;
-
-    let project_root_canonicalized = tokio::fs::canonicalize(config.project_root)
-        .await
-        .expect("Failed to canonicalize project root");
-
-    let mcp_service = MCPServer::new(
-        MCPServerConfig {
-            project_root: project_root_canonicalized,
-        },
-        lsp_server,
-    );
-
-    let server = mcp_service
-        .serve(rmcp::transport::stdio())
-        .await
-        .expect("Failed to starting serving MCP");
-    let (r1, r2) = join!(mainloop_fut, server.waiting());
-    r1.expect("R1 failed");
-
-    r2.expect("R2 failed");
+    let (mcp_server, lsp_client) = mcp::setup(config).await?;
+    mcp_server.run(lsp_client).await?;
 
     Ok(())
 }

src/mcp/mod.rs 🔗

@@ -1,4 +1,4 @@
 pub mod server;
 pub mod tools;
 
-pub use server::{MCPServer, MCPServerConfig};
+pub use server::{MCPServer, setup};

src/mcp/server.rs 🔗

@@ -1,36 +1,70 @@
 use std::path::PathBuf;
 
-use async_lsp::ServerSocket as LSPServerSocket;
-use rmcp::model::CallToolResult;
-use rmcp::ServerHandler as MCPServerHandler;
 use rmcp::ErrorData as MCPError;
+use rmcp::ServerHandler as MCPServerHandler;
 use rmcp::handler::server::tool::ToolRouter;
 use rmcp::handler::server::wrapper::Parameters;
+use rmcp::model::CallToolResult;
 use rmcp::model::{Implementation, ProtocolVersion, ServerCapabilities, ServerInfo};
 use rmcp::{tool_handler, tool_router};
 
-pub use crate::mcp::tools::read::*;
+use async_lsp::ServerSocket as LSPServerSocket;
 
-#[derive(Clone)]
-pub struct MCPServerConfig {
-    pub project_root: PathBuf,
-}
+use crate::config::Config;
+use crate::lsp::LSPClient;
+pub use crate::mcp::tools::read::*;
 
-#[derive(Clone)]
 pub struct MCPServer {
     pub(crate) tool_router: ToolRouter<Self>,
-    pub(crate) config: MCPServerConfig,
+    pub(crate) project_root: PathBuf,
     pub(crate) lsp_server: LSPServerSocket,
 }
 
+pub async fn setup(config: Config) -> anyhow::Result<(MCPServer, LSPClient)> {
+    let project_root = tokio::fs::canonicalize(&config.project_root)
+        .await
+        .expect("Failed to canonicalize project root");
+
+    let lsp_client = LSPClient::setup(
+        &config.lsp_server_command.command,
+        &config.lsp_server_command.args,
+        &project_root,
+    )
+    .await?;
+
+    let server = MCPServer::new(project_root, lsp_client.server.clone());
+
+    Ok((server, lsp_client))
+}
+
 impl MCPServer {
-    pub fn new(config: MCPServerConfig, lsp_server: LSPServerSocket) -> Self {
+    pub fn new(project_root: PathBuf, lsp_server: LSPServerSocket) -> Self {
         Self {
-            config,
+            project_root,
             lsp_server,
             tool_router: Self::tool_router(),
         }
     }
+
+    pub async fn run(self, lsp_client: LSPClient) -> anyhow::Result<()> {
+        let mainloop_fut = tokio::spawn(async move {
+            lsp_client
+                .run_main_loop()
+                .await
+                .expect("Error while running main LSP loop");
+        });
+
+        let server = rmcp::ServiceExt::serve(self, rmcp::transport::stdio())
+            .await
+            .expect("Failed to start serving MCP");
+
+        let (mainloop_result, server_result) = tokio::join!(mainloop_fut, server.waiting());
+
+        mainloop_result?;
+        server_result?;
+
+        Ok(())
+    }
 }
 
 #[tool_router]
@@ -43,7 +77,7 @@ impl MCPServer {
         Parameters(args): Parameters<ReadToolArgs>,
     ) -> Result<CallToolResult, MCPError> {
         crate::mcp::tools::read::call(self, Parameters(args)).await
-	}
+    }
 }
 
 #[tool_handler]

src/mcp/tools/read.rs 🔗

@@ -6,9 +6,7 @@ use async_lsp::lsp_types::{
     TextDocumentIdentifier, Url, WorkDoneProgressParams,
 };
 use rmcp::ErrorData as MCPError;
-use rmcp::{
-    handler::server::wrapper::Parameters, model::CallToolResult, schemars, serde_json,
-};
+use rmcp::{handler::server::wrapper::Parameters, model::CallToolResult, schemars, serde_json};
 
 use crate::mcp::*;
 
@@ -29,7 +27,7 @@ pub async fn call(
     server: &MCPServer,
     Parameters(args): Parameters<ReadToolArgs>,
 ) -> Result<CallToolResult, MCPError> {
-    let file_path = server.config.project_root.join(&args.path);
+    let file_path = server.project_root.join(&args.path);
     let file_path = tokio::fs::canonicalize(file_path).await.unwrap();
     let content = tokio::fs::read_to_string(&file_path)
         .await