Detailed changes
@@ -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"
@@ -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"] }
@@ -5,17 +5,5 @@
"command": "rust-analyzer",
"args": []
}
- },
- "mcp": {
- "lsp2mcp": {
- "type": "stdio",
- "command": "cargo",
- "args": [
- "run",
- "--",
- "--config",
- "sample_config.json"
- ]
- }
}
}
@@ -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)
@@ -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(())
+ }
}
@@ -1,3 +1,3 @@
pub mod client;
-pub use client::setup_lsp_client;
+pub use client::LSPClient;
@@ -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(())
}
@@ -1,4 +1,4 @@
pub mod server;
pub mod tools;
-pub use server::{MCPServer, MCPServerConfig};
+pub use server::{MCPServer, setup};
@@ -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]
@@ -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