@@ -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(())
}