initial commit

Phillip Davis created

Change summary

Cargo.toml         |   2 
crush.json         |  12 +++
sample_config.json |   7 ++
src/main.rs        | 148 +++++++++++++++++++++++++++++++++++------------
4 files changed, 128 insertions(+), 41 deletions(-)

Detailed changes

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"

crush.json 🔗

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

sample_config.json 🔗

@@ -0,0 +1,7 @@
+{
+	"lsp_server_command": {
+		"command": "rust-analyzer",
+		"args": []
+	},
+	"project_root": "/home/phillipdavis/everyday/dev/lsp2mcp"
+}

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<oneshot::Sender<()>>,
 }
 
@@ -30,9 +32,16 @@ mod ControlFlowState {
     pub struct Stop;
 }
 
+#[derive(Clone)]
+struct MCPServerConfig {
+    project_root: PathBuf,
+}
+
 #[derive(Clone)]
 struct MCPServer {
     tool_router: ToolRouter<Self>,
+    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<String>,
+    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<ReadToolArgs>,
     ) -> Result<CallToolResult, MCPError> {
-        // 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<dyn std::error::Error>> {
     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<dyn std::error::Error>> {
     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::<Progress>(|this, params| {
-                tracing::info!("{:?} {:?}", params.token, params.value);
-                ControlFlow::Continue(())
-            })
-            .notification::<PublishDiagnostics>(|this, params| {
-                tracing::info!(
-                    "{:?} {:?} {:?}",
-                    params.uri,
-                    params.version,
-                    params.diagnostics
-                );
-                ControlFlow::Continue(())
-            })
-            .notification::<ShowMessage>(|this, params| {
-                tracing::info!("Message {:?}: {}", params.typ, params.message);
-                ControlFlow::Continue(())
-            })
+            .notification::<Progress>(|this, params| ControlFlow::Continue(()))
+            .notification::<PublishDiagnostics>(|this, params| ControlFlow::Continue(()))
+            .notification::<ShowMessage>(|this, params| ControlFlow::Continue(()))
             .event(|_, _: ControlFlowState::Stop| ControlFlow::Break(Ok(())));
 
         ServiceBuilder::new().service(router)
@@ -137,10 +181,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
         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<dyn std::error::Error>> {
                     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(())
 }