diff --git a/Cargo.lock b/Cargo.lock index 9ef20aa84d3c7d4a3412f3afcd5bc5c31d2dd109..82ae71bb7165477788d91e8fdedb0ab75cecd2a9 100644 --- a/Cargo.lock +++ b/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" diff --git a/Cargo.toml b/Cargo.toml index dd9515d469216ae0e50c2e22c5e85564101756ab..9ace523026ce3d1c117db2c20beef83d5814466a 100644 --- a/Cargo.toml +++ b/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"] } diff --git a/crush.json b/crush.json index 65ff836c964d6bf296eab8d0897d3800d7d1e093..c80e7e1b2fc81eec65eb057c3bacfc8d6d58cd73 100644 --- a/crush.json +++ b/crush.json @@ -5,17 +5,5 @@ "command": "rust-analyzer", "args": [] } - }, - "mcp": { - "lsp2mcp": { - "type": "stdio", - "command": "cargo", - "args": [ - "run", - "--", - "--config", - "sample_config.json" - ] - } } } diff --git a/src/config.rs b/src/config.rs index 6f7465b61f0130913849ff0cdb247fe30c85bc92..55685a22eeddecc4d815dc39daa81f731884f80d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -22,7 +22,7 @@ pub struct Config { } impl Config { - pub async fn load(path: &PathBuf) -> Result> { + pub async fn load(path: &PathBuf) -> anyhow::Result { let config_content = tokio::fs::read_to_string(path).await?; let config: Config = serde_json::from_str(&config_content)?; Ok(config) diff --git a/src/lsp/client.rs b/src/lsp/client.rs index d60f5d4a8c2674930e1f8bb2be21b8235967911f..7acc31a01a9cc0c3a87e69de3a0ae58087a79d3d 100644 --- a/src/lsp/client.rs +++ b/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> { - 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>, + pub server: LSPServerSocket, +} + +impl LSPClient { + pub async fn setup( + lsp_command: &str, + lsp_args: &[String], + project_root: &PathBuf, + ) -> anyhow::Result { + 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::(|_this, _params| ControlFlow::Continue(())) + .notification::(|_this, _params| ControlFlow::Continue(())) + .notification::(|_this, _params| ControlFlow::Continue(())) + .event(|_, _: control_flow_state::Stop| ControlFlow::Break(Ok(()))); - router - .notification::(|_this, _params| ControlFlow::Continue(())) - .notification::(|_this, _params| ControlFlow::Continue(())) - .notification::(|_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(()) + } } diff --git a/src/lsp/mod.rs b/src/lsp/mod.rs index b7f940d2984a5ac9197de84bc10356c527e001d9..5f542a7204610beaea0cc205b1f7ba865f3aa6ef 100644 --- a/src/lsp/mod.rs +++ b/src/lsp/mod.rs @@ -1,3 +1,3 @@ pub mod client; -pub use client::setup_lsp_client; +pub use client::LSPClient; diff --git a/src/main.rs b/src/main.rs index 7acb550ddefeae7289d2d0e94d72a7f880f6dd75..b0e6b5f1a3f9ba7e75cb9d32787ea09093705835 100644 --- a/src/main.rs +++ b/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> { +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(()) } diff --git a/src/mcp/mod.rs b/src/mcp/mod.rs index 696238425ff55006929950dbcb289ee71ac6b09a..f834260995e0e6d5b07f24645f13189545c83651 100644 --- a/src/mcp/mod.rs +++ b/src/mcp/mod.rs @@ -1,4 +1,4 @@ pub mod server; pub mod tools; -pub use server::{MCPServer, MCPServerConfig}; +pub use server::{MCPServer, setup}; diff --git a/src/mcp/server.rs b/src/mcp/server.rs index a10c0f2182a3c42f218fa7553b603b2f5ad072ea..c2275f3afd65ab6b3c68e9777c87bfd635aa4dbe 100644 --- a/src/mcp/server.rs +++ b/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, - 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, ) -> Result { crate::mcp::tools::read::call(self, Parameters(args)).await - } + } } #[tool_handler] diff --git a/src/mcp/tools/read.rs b/src/mcp/tools/read.rs index b8d8761a728549e3aa32edea0fb1cd6a70ec972d..84b08a8fa4804b4feab06817ad78b4508f9cac9c 100644 --- a/src/mcp/tools/read.rs +++ b/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, ) -> Result { - 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