From c0affbe369d26abf7cb1cfadef2cd0bea03e2d81 Mon Sep 17 00:00:00 2001 From: Phillip Davis Date: Sat, 4 Oct 2025 12:27:52 -0400 Subject: [PATCH] initial commit --- Cargo.toml | 2 +- crush.json | 12 ++++ sample_config.json | 7 +++ src/main.rs | 148 +++++++++++++++++++++++++++++++++------------ 4 files changed, 128 insertions(+), 41 deletions(-) create mode 100644 sample_config.json diff --git a/Cargo.toml b/Cargo.toml index a3cc741366cea7ae87dd8d945a0ad1ac553f02b7..dd9515d469216ae0e50c2e22c5e85564101756ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ async-lsp = "0.2.2" async-process = "2.3" clap = { version = "4.5.48", features = ["derive"] } futures = "0.3.31" -rmcp = { version = "0.8.0", features = ["server"] } +rmcp = { version = "0.8.0", features = ["server", "transport-io"] } serde = { version = "1.0", features = ["derive"] } tokio = { version = "1.47.1", features = ["macros", "rt", "rt-multi-thread", "fs"] } toml = "0.8" diff --git a/crush.json b/crush.json index c80e7e1b2fc81eec65eb057c3bacfc8d6d58cd73..65ff836c964d6bf296eab8d0897d3800d7d1e093 100644 --- a/crush.json +++ b/crush.json @@ -5,5 +5,17 @@ "command": "rust-analyzer", "args": [] } + }, + "mcp": { + "lsp2mcp": { + "type": "stdio", + "command": "cargo", + "args": [ + "run", + "--", + "--config", + "sample_config.json" + ] + } } } diff --git a/sample_config.json b/sample_config.json new file mode 100644 index 0000000000000000000000000000000000000000..7b07076ffc8934038a8a7244a7d2616a22038044 --- /dev/null +++ b/sample_config.json @@ -0,0 +1,7 @@ +{ + "lsp_server_command": { + "command": "rust-analyzer", + "args": [] + }, + "project_root": "/home/phillipdavis/everyday/dev/lsp2mcp" +} diff --git a/src/main.rs b/src/main.rs index 79c67da0e66b2e66246e2026e156fc3db6650168..cb89df4d6ad4c445ad43ccd27e59144d67c7ba94 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,28 +1,30 @@ use std::ops::ControlFlow; use std::path::PathBuf; -use async_lsp::concurrency::ConcurrencyLayer; use async_lsp::lsp_types::notification::{Progress, PublishDiagnostics, ShowMessage}; use async_lsp::lsp_types::{ - ClientCapabilities, DidOpenTextDocumentParams, InitializeParams, InitializedParams, Url, - WindowClientCapabilities, WorkspaceFolder, + ClientCapabilities, DiagnosticClientCapabilities, DocumentDiagnosticParams, + DocumentDiagnosticReportResult, InitializeParams, InitializedParams, PartialResultParams, + TextDocumentClientCapabilities, TextDocumentIdentifier, Url, WindowClientCapabilities, + WorkDoneProgressParams, WorkspaceFolder, }; -use async_lsp::panic::CatchUnwindLayer; use async_lsp::router::Router; -use async_lsp::tracing::TracingLayer; -use async_lsp::{Error, ErrorCode, LanguageServer}; +use async_lsp::{LanguageServer, ServerSocket as LSPServerSocket}; use async_process::{Command as ProcessCommand, Stdio}; use clap::Parser; use futures::channel::oneshot; -use rmcp::ServerHandler as MCPServerHandler; +use futures::join; use rmcp::handler::server::tool::ToolRouter; use rmcp::handler::server::wrapper::Parameters; -use rmcp::model::CallToolResult; -use rmcp::{ErrorData as MCPError, schemars}; +use rmcp::model::{ + CallToolResult, Implementation, ProtocolVersion, ServerCapabilities, ServerInfo, +}; +use rmcp::{ErrorData as MCPError, ServiceExt, schemars}; +use rmcp::{ServerHandler as MCPServerHandler, serde_json}; use rmcp::{tool, tool_handler, tool_router}; use tower::ServiceBuilder; -struct ClientState { +struct LSPClientState { indexed_tx: Option>, } @@ -30,9 +32,16 @@ mod ControlFlowState { pub struct Stop; } +#[derive(Clone)] +struct MCPServerConfig { + project_root: PathBuf, +} + #[derive(Clone)] struct MCPServer { tool_router: ToolRouter, + config: MCPServerConfig, + lsp_server: LSPServerSocket, } #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] @@ -42,23 +51,61 @@ struct ReadToolArgs { range_end: usize, } -struct ReadToolOutput {} +#[derive(Debug, serde::Serialize)] +struct ReadToolOutput { + content: Option, + diagnostics: DocumentDiagnosticReportResult, +} #[tool_router] impl MCPServer { /// Description take from Amp's description of their built-in `read` tool - #[tool(description = "Read a file or list a directory from the file system")] + #[tool( + description = "Like the 'view' tool, except it returns LSP diagnostics too. Always use this instead of 'view'" + )] async fn read( &self, Parameters(args): Parameters, ) -> Result { - // TODO: Read the file in the file system - // TODO: Push "didOpenFile" to LSP - // TODO: Get diagnostics from LSP + let file_path = self.config.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 + .map_err(|e| MCPError::invalid_request(format!("Failed to read file: {e}"), None))?; + + let diagnostic_report = self + .lsp_server + .clone() + .document_diagnostic(DocumentDiagnosticParams { + text_document: TextDocumentIdentifier::new(Url::from_file_path(file_path).unwrap()), + identifier: None, + previous_result_id: None, + work_done_progress_params: WorkDoneProgressParams { + work_done_token: None, + }, + partial_result_params: PartialResultParams { + partial_result_token: None, + }, + }) + .await + .unwrap(); + + // self.lsp_server.clone().did_open(DidOpenTextDocumentParams { + // text_document: TextDocumentItem { + // uri: Url::from_file_path(file_path).unwrap(), + // language_id: "rust".into(), + // version: 0, + // text: content + // } + // }).unwrap(); + // TODO: Construct and return result Ok(CallToolResult { content: vec![], - structured_content: None, + structured_content: Some(serde_json::json!(ReadToolOutput { + content: Some(content), + diagnostics: diagnostic_report + })), is_error: None, meta: None, }) @@ -66,9 +113,18 @@ impl MCPServer { } #[tool_handler] -impl MCPServerHandler for MCPServer {} +impl MCPServerHandler for MCPServer { + fn get_info(&self) -> ServerInfo { + ServerInfo { + protocol_version: ProtocolVersion::V_2024_11_05, + capabilities: ServerCapabilities::builder().enable_tools().build(), + server_info: Implementation::from_build_env(), + instructions: Some("This server turns standard coding agent tools like `read` and `edit` into LSP clients.".into()) + } + } +} -#[derive(clap::Parser)] +#[derive(clap::Parser, Debug)] #[command(version, about)] struct CommandLineArgs { #[arg(short, long, value_name = "FILE")] @@ -92,7 +148,9 @@ async fn main() -> Result<(), Box> { let args = CommandLineArgs::parse(); let config_content = tokio::fs::read_to_string(&args.config).await?; - let config: Config = toml::from_str(&config_content)?; + + let config: Config = + serde_json::from_str(&config_content).expect("Failed to parse config file"); let mut child = ProcessCommand::new(&config.lsp_server_command.command) .args(&config.lsp_server_command.args) @@ -108,26 +166,12 @@ async fn main() -> Result<(), Box> { let stdout = child.stdout.take().unwrap(); let (mainloop, mut server) = async_lsp::MainLoop::new_client(|server| { - let mut router = Router::new(ClientState { indexed_tx: None }); + let mut router = Router::new(LSPClientState { indexed_tx: None }); router - .notification::(|this, params| { - tracing::info!("{:?} {:?}", params.token, params.value); - ControlFlow::Continue(()) - }) - .notification::(|this, params| { - tracing::info!( - "{:?} {:?} {:?}", - params.uri, - params.version, - params.diagnostics - ); - ControlFlow::Continue(()) - }) - .notification::(|this, params| { - tracing::info!("Message {:?}: {}", params.typ, params.message); - ControlFlow::Continue(()) - }) + .notification::(|this, params| ControlFlow::Continue(())) + .notification::(|this, params| ControlFlow::Continue(())) + .notification::(|this, params| ControlFlow::Continue(())) .event(|_, _: ControlFlowState::Stop| ControlFlow::Break(Ok(()))); ServiceBuilder::new().service(router) @@ -137,10 +181,14 @@ async fn main() -> Result<(), Box> { mainloop.run_buffered(stdout, stdin).await.unwrap(); }); + let project_root_canonicalized = tokio::fs::canonicalize(config.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: Url::from_file_path(&config.project_root).unwrap(), + uri: uri, name: "root".into(), }]), capabilities: ClientCapabilities { @@ -148,16 +196,36 @@ async fn main() -> Result<(), Box> { work_done_progress: Some(true), ..WindowClientCapabilities::default() }), + text_document: Some(TextDocumentClientCapabilities { + diagnostic: Some(DiagnosticClientCapabilities { + ..DiagnosticClientCapabilities::default() + }), + ..TextDocumentClientCapabilities::default() + }), ..ClientCapabilities::default() }, ..InitializeParams::default() }) .await .unwrap(); - tracing::info!("Initialized: {init_ret:?}"); server.initialized(InitializedParams {}).unwrap(); - mainloop_fut.await.unwrap(); + let mcp_service = MCPServer { + config: MCPServerConfig { + project_root: project_root_canonicalized, + }, + tool_router: MCPServer::tool_router(), + lsp_server: 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"); Ok(()) }