From 9c646f6d3df10efaaef734920e4210b53653dc90 Mon Sep 17 00:00:00 2001 From: Phillip Davis Date: Thu, 9 Oct 2025 09:08:22 -0400 Subject: [PATCH] wip: BufPool improvements + document sync semi-vibed, need to clean up to match code conventions. - replace all the `Result`s with `anyhow::Result`s - I don't like that `LSPClient::setup` returns both a server and a client, seems like it should just be at the root of the `lsp` module - gotta remove a bunch of lints that are annoying and eating tokens - gotta remove all the dumb agent comments - eventually we'll have to be clever about de-allocating memory, since at this point the LRU strategy used by `OpenedDocuments` never actually deallocates. This logic will have to happen in `BufPool.` - move the `LanguageId` type to its own module - I think we should refactor all file access to go through the LSP, i.e., `OpenedDocuments` should be able to read the document into a buffer or something, and this should probably be accessed through `LSPServer`. Basically, the read tool itself will be very minimal. This ensures that all the LSP bookkeeping and doc synchronization is handled correctly. --- Cargo.lock | 60 +++++++- Cargo.toml | 1 + src/buf_pool.rs | 63 ++++++--- src/config.rs | 4 +- src/lsp/client.rs | 20 ++- src/lsp/mod.rs | 3 + src/lsp/opened_documents.rs | 131 +++++++++++++++++ src/lsp/server.rs | 272 ++++++++++++++++++++++++++++++++++++ src/mcp/server.rs | 14 +- src/mcp/tools/read.rs | 7 + 10 files changed, 541 insertions(+), 34 deletions(-) create mode 100644 src/lsp/opened_documents.rs create mode 100644 src/lsp/server.rs diff --git a/Cargo.lock b/Cargo.lock index 916b872d69e76025efe70c103ce2ac8181840ad4..792bd0dd986172f898df3382b6535a38426fafc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -463,6 +463,20 @@ dependencies = [ "syn", ] +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -652,6 +666,12 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "hashbrown" version = "0.16.0" @@ -814,7 +834,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.0", ] [[package]] @@ -874,6 +894,15 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.28" @@ -903,6 +932,7 @@ dependencies = [ "async-process", "clap", "crossbeam", + "dashmap", "futures", "rmcp", "serde", @@ -993,6 +1023,19 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + [[package]] name = "paste" version = "1.0.15" @@ -1069,6 +1112,15 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.9.4", +] + [[package]] name = "ref-cast" version = "1.0.25" @@ -1197,6 +1249,12 @@ dependencies = [ "syn", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.228" diff --git a/Cargo.toml b/Cargo.toml index e188d3c73f4c6bf569514d56e25846d9d112e0dc..1bfc8c58ba7367e34191a38a321b12454cad9c6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ async-lsp = "0.2.2" async-process = "2.3" clap = { version = "4.5.48", features = ["derive"] } crossbeam = "0.8" +dashmap = "6.1.0" futures = "0.3.31" rmcp = { version = "0.8.0", features = ["server", "transport-io"] } serde = { version = "1.0", features = ["derive"] } diff --git a/src/buf_pool.rs b/src/buf_pool.rs index 6c9b5308d05cb132a63bf4a80a6f785062d5afaf..ca07e64c7e7d3e42e43fa73e83c1eb397d9873c0 100644 --- a/src/buf_pool.rs +++ b/src/buf_pool.rs @@ -1,26 +1,56 @@ use crossbeam::queue::SegQueue; use std::ops::{Deref, DerefMut}; +use std::path::PathBuf; use std::sync::Arc; -/// A thread-safe, lock-free pool of reusable byte buffers. +/// Trait for types that can be pooled and reused. +/// +/// Types implementing this trait can be: +/// 1. Created empty via `Default` +/// 2. Reset to empty state via `clear()` (preserving capacity) +/// 3. Sent across threads (required by `SegQueue`) +pub trait Poolable: Default + Send { + /// Reset the value to its empty state, preserving any allocated capacity. + fn clear(&mut self); +} + +impl Poolable for Vec { + fn clear(&mut self) { + Vec::clear(self); + } +} + +impl Poolable for String { + fn clear(&mut self) { + String::clear(self); + } +} + +impl Poolable for PathBuf { + fn clear(&mut self) { + PathBuf::clear(self); + } +} + +/// A thread-safe, lock-free pool of reusable buffers. #[derive(Clone)] -pub struct BufPool { - queue: Arc>>, +pub struct BufPool { + queue: Arc>, } -impl BufPool { +impl BufPool { /// Creates a pool pre-populated with `capacity` empty buffers. pub fn with_capacity(capacity: usize) -> Self { let queue = Arc::new(SegQueue::new()); for _ in 0..capacity { - queue.push(Vec::new()); + queue.push(T::default()); } Self { queue } } /// Checks out a buffer from the pool. If the pool is empty, allocates a new buffer. - pub fn checkout(&self) -> BufGuard { - let buf = self.queue.pop().unwrap_or_else(|| Vec::new()); + pub fn checkout(&self) -> BufGuard { + let buf = self.queue.pop().unwrap_or_else(T::default); BufGuard { buf, pool: self.queue.clone(), @@ -29,15 +59,16 @@ impl BufPool { } /// RAII guard that automatically returns the buffer to the pool when dropped. -pub struct BufGuard { - buf: Vec, - pool: Arc>>, +#[derive(Debug)] +pub struct BufGuard { + buf: T, + pool: Arc>, } -impl BufGuard { +impl BufGuard { /// Extracts the buffer, preventing automatic return to the pool. /// Useful if you need to move the buffer elsewhere. - pub fn into_inner(mut self) -> Vec { + pub fn into_inner(mut self) -> T { let buf = std::mem::take(&mut self.buf); // SAFETY: // 1) We forget `self`, preventing it from @@ -51,7 +82,7 @@ impl BufGuard { } } -impl Drop for BufGuard { +impl Drop for BufGuard { fn drop(&mut self) { let mut buf = std::mem::take(&mut self.buf); buf.clear(); @@ -59,15 +90,15 @@ impl Drop for BufGuard { } } -impl Deref for BufGuard { - type Target = Vec; +impl Deref for BufGuard { + type Target = T; fn deref(&self) -> &Self::Target { &self.buf } } -impl DerefMut for BufGuard { +impl DerefMut for BufGuard { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.buf } diff --git a/src/config.rs b/src/config.rs index b1a954a27ff24a63f256eb2aa283f51ebe82c709..2bf4410ec407687d46abcf6738166a127ebf13e8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,3 @@ -use anyhow::Context; -use async_gen::{AsyncIter, r#gen}; use clap::Parser; use rmcp::serde_json; use std::{ @@ -7,6 +5,8 @@ use std::{ path::{Path, PathBuf}, }; +use crate::buf_pool::BufPool; + #[derive(Parser, Debug)] #[command(version, about)] pub struct CommandLineArgs { diff --git a/src/lsp/client.rs b/src/lsp/client.rs index b3b157ab7a36954d2cb098ac78fb2030e3cb5a68..683155d080aa2109d582ea8e06168301ea6e087d 100644 --- a/src/lsp/client.rs +++ b/src/lsp/client.rs @@ -14,6 +14,7 @@ use futures::channel::oneshot; use tower::ServiceBuilder; use crate::config::Config; +use crate::lsp::server::LSPServer; pub struct LSPClientState { pub config: Arc, @@ -27,11 +28,11 @@ mod control_flow_state { pub struct LSPClient { child: Child, pub mainloop: MainLoop>, - pub server: LSPServerSocket, + socket: LSPServerSocket, } impl LSPClient { - pub async fn setup(config: Arc) -> anyhow::Result { + pub async fn setup(config: Arc) -> anyhow::Result<(Self, LSPServer)> { let child = ProcessCommand::new(&config.lsp_server_command.command) .args(&config.lsp_server_command.args) .current_dir(&config.project_root) @@ -99,11 +100,16 @@ impl LSPClient { .initialized(InitializedParams {}) .expect("Bad response to Initialized message"); - Ok(Self { - child, - mainloop, - server, - }) + let lsp_server = LSPServer::new(server.clone()); + + Ok(( + Self { + child, + mainloop, + socket: server, + }, + lsp_server, + )) } pub async fn run_main_loop(mut self) -> anyhow::Result<()> { diff --git a/src/lsp/mod.rs b/src/lsp/mod.rs index 5f542a7204610beaea0cc205b1f7ba865f3aa6ef..afe267d2272bc7e0c5810b37cc8479b4eed8ac92 100644 --- a/src/lsp/mod.rs +++ b/src/lsp/mod.rs @@ -1,3 +1,6 @@ pub mod client; +pub mod opened_documents; +pub mod server; pub use client::LSPClient; +pub use server::LSPServer; diff --git a/src/lsp/opened_documents.rs b/src/lsp/opened_documents.rs new file mode 100644 index 0000000000000000000000000000000000000000..3d3013f73ed8f21f251d52fca16961a47f2823e6 --- /dev/null +++ b/src/lsp/opened_documents.rs @@ -0,0 +1,131 @@ +use dashmap::DashMap; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::time::Instant; + +use crate::buf_pool::{BufGuard, BufPool}; + +/// Represents an opened document tracked by the LSP client. +#[derive(Debug)] +pub struct OpenedDocument { + pub version: u32, + pub content: BufGuard, + pub last_accessed: Instant, +} + +/// Thread-safe tracker for opened LSP documents. +/// +/// IMPORTANT: All paths passed to this type must be canonical to avoid +/// duplicate entries (e.g., "/foo//bar" vs "/foo/bar"). +#[derive(Clone)] +pub struct OpenedDocuments { + docs: Arc>, + string_pool: BufPool, +} + +impl OpenedDocuments { + pub fn new() -> Self { + Self { + docs: Arc::new(DashMap::new()), + string_pool: BufPool::with_capacity(50), + } + } + + /// Validate that a path appears to be canonical. + /// A canonical path must be absolute and not contain `.` or `..` components. + fn validate_canonical(path: &Path) -> anyhow::Result<()> { + if !path.is_absolute() { + anyhow::bail!("Path must be canonical (absolute): {}", path.display()); + } + + // Check for . or .. components + for component in path.components() { + match component { + std::path::Component::CurDir | std::path::Component::ParentDir => { + anyhow::bail!("Path must be canonical (no . or .. components): {}", path.display()); + } + _ => {} + } + } + + Ok(()) + } + + /// Check if a document is currently opened. + pub fn is_open(&self, path: impl AsRef) -> anyhow::Result { + let path = path.as_ref(); + Self::validate_canonical(path)?; + Ok(self.docs.contains_key(path)) + } + + /// Get the version number of an opened document. + pub fn get_version(&self, path: impl AsRef) -> anyhow::Result> { + let path = path.as_ref(); + Self::validate_canonical(path)?; + Ok(self.docs.get(path).map(|doc| doc.version)) + } + + /// Insert a new opened document. Path must be canonical. + pub fn insert(&self, path: PathBuf, content: &str, version: u32) -> anyhow::Result<()> { + Self::validate_canonical(&path)?; + let mut buf = self.string_pool.checkout(); + buf.push_str(content); + self.docs.insert( + path, + OpenedDocument { + version, + content: buf, + last_accessed: Instant::now(), + }, + ); + Ok(()) + } + + /// Update the last_accessed timestamp for a document. + pub fn touch(&self, path: impl AsRef) -> anyhow::Result<()> { + let path = path.as_ref(); + Self::validate_canonical(path)?; + if let Some(mut doc) = self.docs.get_mut(path) { + doc.last_accessed = Instant::now(); + } + Ok(()) + } + + /// Remove a document from tracking. + pub fn remove(&self, path: impl AsRef) -> anyhow::Result> { + let path = path.as_ref(); + Self::validate_canonical(path)?; + Ok(self.docs.remove(path).map(|(_, doc)| doc)) + } + + /// Evict the least recently used documents, keeping only `keep_count` documents. + /// Returns the paths of evicted documents. + pub fn evict_lru(&self, keep_count: usize) -> Vec { + let current_count = self.docs.len(); + if current_count <= keep_count { + return Vec::new(); + } + + let evict_count = current_count - keep_count; + + // Collect all documents with their access times + let mut docs_with_time: Vec<(PathBuf, Instant)> = self + .docs + .iter() + .map(|entry| (entry.key().clone(), entry.value().last_accessed)) + .collect(); + + // Sort by last_accessed (oldest first) + docs_with_time.sort_by_key(|(_, time)| *time); + + // Evict the oldest documents + let mut evicted = Vec::new(); + for (path, _) in docs_with_time.into_iter().take(evict_count) { + if self.docs.remove(&path).is_some() { + evicted.push(path); + } + } + + evicted + } +} diff --git a/src/lsp/server.rs b/src/lsp/server.rs new file mode 100644 index 0000000000000000000000000000000000000000..4c27dd31d63e69cfc5024737f11e6d2823e6e97a --- /dev/null +++ b/src/lsp/server.rs @@ -0,0 +1,272 @@ +use async_lsp::lsp_types::{ + DidCloseTextDocumentParams, DidOpenTextDocumentParams, DocumentDiagnosticParams, + DocumentSymbolParams, DocumentSymbolResponse, TextDocumentIdentifier, TextDocumentItem, + Url, +}; +use async_lsp::{LanguageServer, ServerSocket as LSPServerSocket}; +use rmcp::ErrorData as MCPError; +use std::path::Path; + +use crate::lsp::opened_documents::OpenedDocuments; + +const MAX_OPEN_DOCUMENTS: usize = 50; + +/// Language identifiers for LSP textDocument/didOpen. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LanguageId { + Rust, + Python, + JavaScript, + TypeScript, + JavaScriptReact, + TypeScriptReact, + Go, + Java, + C, + Cpp, + CSharp, + Ruby, + Php, + Swift, + Kotlin, + Scala, + Html, + Css, + Scss, + Less, + Json, + Xml, + Yaml, + Toml, + Markdown, + Shell, + /// For languages not explicitly enumerated or unknown file types. + Other(String), +} + +impl LanguageId { + /// Infer language ID from file path extension. + pub fn from_path(path: &Path) -> Self { + match path.extension().and_then(|e| e.to_str()) { + Some("rs") => Self::Rust, + Some("py") | Some("pyi") => Self::Python, + Some("js") | Some("mjs") | Some("cjs") => Self::JavaScript, + Some("ts") | Some("mts") | Some("cts") => Self::TypeScript, + Some("jsx") => Self::JavaScriptReact, + Some("tsx") => Self::TypeScriptReact, + Some("go") => Self::Go, + Some("java") => Self::Java, + Some("c") | Some("h") => Self::C, + Some("cpp") | Some("cc") | Some("cxx") | Some("hpp") | Some("hh") | Some("hxx") => Self::Cpp, + Some("cs") => Self::CSharp, + Some("rb") => Self::Ruby, + Some("php") => Self::Php, + Some("swift") => Self::Swift, + Some("kt") | Some("kts") => Self::Kotlin, + Some("scala") | Some("sc") => Self::Scala, + Some("html") | Some("htm") => Self::Html, + Some("css") => Self::Css, + Some("scss") => Self::Scss, + Some("less") => Self::Less, + Some("json") | Some("jsonc") => Self::Json, + Some("xml") => Self::Xml, + Some("yaml") | Some("yml") => Self::Yaml, + Some("toml") => Self::Toml, + Some("md") | Some("markdown") => Self::Markdown, + Some("sh") | Some("bash") | Some("zsh") => Self::Shell, + _ => Self::Other("plaintext".into()), + } + } + + /// Convert to LSP-compliant language identifier string. + pub fn as_lsp_identifier(&self) -> &str { + match self { + Self::Rust => "rust", + Self::Python => "python", + Self::JavaScript => "javascript", + Self::TypeScript => "typescript", + Self::JavaScriptReact => "javascriptreact", + Self::TypeScriptReact => "typescriptreact", + Self::Go => "go", + Self::Java => "java", + Self::C => "c", + Self::Cpp => "cpp", + Self::CSharp => "csharp", + Self::Ruby => "ruby", + Self::Php => "php", + Self::Swift => "swift", + Self::Kotlin => "kotlin", + Self::Scala => "scala", + Self::Html => "html", + Self::Css => "css", + Self::Scss => "scss", + Self::Less => "less", + Self::Json => "json", + Self::Xml => "xml", + Self::Yaml => "yaml", + Self::Toml => "toml", + Self::Markdown => "markdown", + Self::Shell => "shellscript", + Self::Other(s) => s.as_str(), + } + } +} + +/// Wrapper around LSPServerSocket that manages document lifecycle. +#[derive(Clone)] +pub struct LSPServer { + socket: LSPServerSocket, + opened_docs: OpenedDocuments, +} + +impl LSPServer { + pub fn new(socket: LSPServerSocket) -> Self { + Self { + socket, + opened_docs: OpenedDocuments::new(), + } + } + + /// Open a document with the LSP server. Path must be canonical. + /// If the document is already open, this is a no-op (but updates last_accessed). + pub async fn open_document(&self, path: impl AsRef, content: &str) -> Result<(), MCPError> { + let path = path.as_ref(); + + // Check if already open + if self.opened_docs.is_open(path) + .map_err(|e| MCPError::internal_error(e.to_string(), None))? + { + self.opened_docs.touch(path) + .map_err(|e| MCPError::internal_error(e.to_string(), None))?; + return Ok(()); + } + + // Infer language from file extension + let language_id = LanguageId::from_path(path); + + // Convert path to URI + let uri = Url::from_file_path(path) + .map_err(|_| MCPError::invalid_params(format!("Invalid file path: {}", path.display()), None))?; + + // Send didOpen notification + self.socket + .clone() + .did_open(DidOpenTextDocumentParams { + text_document: TextDocumentItem { + uri: uri.clone(), + language_id: language_id.as_lsp_identifier().to_string(), + version: 0, + text: content.to_string(), + }, + }) + .map_err(|e| MCPError::internal_error(format!("Failed to send didOpen: {}", e), None))?; + + // Track the document + self.opened_docs + .insert(path.to_path_buf(), content, 0) + .map_err(|e| MCPError::internal_error(e.to_string(), None))?; + + // Evict old documents if needed + self.evict_old_documents().await?; + + Ok(()) + } + + /// Close a document with the LSP server. Path must be canonical. + pub async fn close_document(&self, path: impl AsRef) -> Result<(), MCPError> { + let path = path.as_ref(); + + // Convert path to URI + let uri = Url::from_file_path(path) + .map_err(|_| MCPError::invalid_params(format!("Invalid file path: {}", path.display()), None))?; + + // Send didClose notification + self.socket + .clone() + .did_close(DidCloseTextDocumentParams { + text_document: TextDocumentIdentifier { uri }, + }) + .map_err(|e| MCPError::internal_error(format!("Failed to send didClose: {}", e), None))?; + + // Remove from tracking + self.opened_docs + .remove(path) + .map_err(|e| MCPError::internal_error(e.to_string(), None))?; + + Ok(()) + } + + /// Evict least recently used documents to keep count under MAX_OPEN_DOCUMENTS. + async fn evict_old_documents(&self) -> Result<(), MCPError> { + let evicted = self.opened_docs.evict_lru(MAX_OPEN_DOCUMENTS); + + for path in evicted { + // Close with LSP server + let uri = Url::from_file_path(&path) + .map_err(|_| MCPError::internal_error(format!("Invalid file path: {}", path.display()), None))?; + + self.socket + .clone() + .did_close(DidCloseTextDocumentParams { + text_document: TextDocumentIdentifier { uri }, + }) + .map_err(|e| MCPError::internal_error(format!("Failed to send didClose during eviction: {}", e), None))?; + } + + Ok(()) + } + + /// Get diagnostics for a document. Path must be canonical. + pub async fn document_diagnostic( + &self, + path: impl AsRef, + ) -> Result { + let path = path.as_ref(); + + // Touch to update LRU + self.opened_docs + .touch(path) + .map_err(|e| MCPError::internal_error(e.to_string(), None))?; + + let uri = Url::from_file_path(path) + .map_err(|_| MCPError::invalid_params(format!("Invalid file path: {}", path.display()), None))?; + + self.socket + .clone() + .document_diagnostic(DocumentDiagnosticParams { + text_document: TextDocumentIdentifier { uri }, + identifier: None, + previous_result_id: None, + work_done_progress_params: Default::default(), + partial_result_params: Default::default(), + }) + .await + .map_err(|e| MCPError::internal_error(format!("Failed to get diagnostics: {}", e), None)) + } + + /// Get document symbols (outline). Path must be canonical. + pub async fn document_symbols( + &self, + path: impl AsRef, + ) -> Result, MCPError> { + let path = path.as_ref(); + + // Touch to update LRU + self.opened_docs + .touch(path) + .map_err(|e| MCPError::internal_error(e.to_string(), None))?; + + let uri = Url::from_file_path(path) + .map_err(|_| MCPError::invalid_params(format!("Invalid file path: {}", path.display()), None))?; + + self.socket + .clone() + .document_symbol(DocumentSymbolParams { + text_document: TextDocumentIdentifier { uri }, + work_done_progress_params: Default::default(), + partial_result_params: Default::default(), + }) + .await + .map_err(|e| MCPError::internal_error(format!("Failed to get document symbols: {}", e), None)) + } +} diff --git a/src/mcp/server.rs b/src/mcp/server.rs index 3793c818f8dfa8f48a7f499ca0948a534653b3ec..028eaa33abd2982a8767a80fc646ea9e6330c9a8 100644 --- a/src/mcp/server.rs +++ b/src/mcp/server.rs @@ -9,30 +9,28 @@ use rmcp::model::CallToolResult; use rmcp::model::{Implementation, ProtocolVersion, ServerCapabilities, ServerInfo}; use rmcp::{tool_handler, tool_router}; -use async_lsp::ServerSocket as LSPServerSocket; - use crate::buf_pool::BufPool; use crate::config::Config; -use crate::lsp::LSPClient; +use crate::lsp::{LSPClient, LSPServer}; pub use crate::mcp::tools::read::*; pub struct MCPServer { pub(crate) tool_router: ToolRouter, pub(crate) config: Arc, - pub(crate) lsp_server: LSPServerSocket, - pub(crate) buf_pool: BufPool, + pub(crate) lsp_server: LSPServer, + pub(crate) buf_pool: BufPool>, } pub async fn setup(config: Arc) -> anyhow::Result<(MCPServer, LSPClient)> { - let lsp_client = LSPClient::setup(config.clone()).await?; + let (lsp_client, lsp_server) = LSPClient::setup(config.clone()).await?; - let server = MCPServer::new(config, lsp_client.server.clone()); + let server = MCPServer::new(config, lsp_server); Ok((server, lsp_client)) } impl MCPServer { - pub fn new(config: Arc, lsp_server: LSPServerSocket) -> Self { + pub fn new(config: Arc, lsp_server: LSPServer) -> Self { Self { config, lsp_server, diff --git a/src/mcp/tools/read.rs b/src/mcp/tools/read.rs index 25753bc0a7def4658dbbf58ac1caec10175614b1..b465a3f326872a0a0907b35b2d046e5e1072f539 100644 --- a/src/mcp/tools/read.rs +++ b/src/mcp/tools/read.rs @@ -9,6 +9,7 @@ use async_lsp::lsp_types::{ }; use rmcp::ErrorData as MCPError; use rmcp::{handler::server::wrapper::Parameters, model::CallToolResult, schemars, serde_json}; +use tokio::io::AsyncReadExt; // for read_to_end() use crate::mcp::*; @@ -38,6 +39,12 @@ pub async fn call( MCPError::invalid_params(format!("Path not in project: {:?}", &args.path), None) })?; + let file = tokio::fs::File::open(file_path.as_path()) + .await + .map_err(|e| MCPError::internal_error(format!("Failed to open file: {e:?}"), None))?; + + let file_buf = server.buf_pool.checkout(); + Err(MCPError::internal_error("Not yet implemented", None)) // let content = tokio::fs::read_to_string(&file_path) // .await