Detailed changes
@@ -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"
@@ -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"] }
@@ -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<T: Send> Poolable for Vec<T> {
+ 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<SegQueue<Vec<u8>>>,
+pub struct BufPool<T: Poolable> {
+ queue: Arc<SegQueue<T>>,
}
-impl BufPool {
+impl<T: Poolable> BufPool<T> {
/// 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<T> {
+ 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<u8>,
- pool: Arc<SegQueue<Vec<u8>>>,
+#[derive(Debug)]
+pub struct BufGuard<T: Poolable> {
+ buf: T,
+ pool: Arc<SegQueue<T>>,
}
-impl BufGuard {
+impl<T: Poolable> BufGuard<T> {
/// 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<u8> {
+ 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<T: Poolable> Drop for BufGuard<T> {
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<u8>;
+impl<T: Poolable> Deref for BufGuard<T> {
+ type Target = T;
fn deref(&self) -> &Self::Target {
&self.buf
}
}
-impl DerefMut for BufGuard {
+impl<T: Poolable> DerefMut for BufGuard<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.buf
}
@@ -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 {
@@ -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<Config>,
@@ -27,11 +28,11 @@ mod control_flow_state {
pub struct LSPClient {
child: Child,
pub mainloop: MainLoop<Router<LSPClientState>>,
- pub server: LSPServerSocket,
+ socket: LSPServerSocket,
}
impl LSPClient {
- pub async fn setup(config: Arc<Config>) -> anyhow::Result<Self> {
+ pub async fn setup(config: Arc<Config>) -> 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<()> {
@@ -1,3 +1,6 @@
pub mod client;
+pub mod opened_documents;
+pub mod server;
pub use client::LSPClient;
+pub use server::LSPServer;
@@ -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<String>,
+ 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<DashMap<PathBuf, OpenedDocument>>,
+ string_pool: BufPool<String>,
+}
+
+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<Path>) -> anyhow::Result<bool> {
+ 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<Path>) -> anyhow::Result<Option<u32>> {
+ 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<Path>) -> 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<Path>) -> anyhow::Result<Option<OpenedDocument>> {
+ 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<PathBuf> {
+ 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
+ }
+}
@@ -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<Path>, 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<Path>) -> 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<Path>,
+ ) -> Result<async_lsp::lsp_types::DocumentDiagnosticReportResult, 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_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<Path>,
+ ) -> Result<Option<DocumentSymbolResponse>, 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))
+ }
+}
@@ -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<Self>,
pub(crate) config: Arc<Config>,
- pub(crate) lsp_server: LSPServerSocket,
- pub(crate) buf_pool: BufPool,
+ pub(crate) lsp_server: LSPServer,
+ pub(crate) buf_pool: BufPool<Vec<u8>>,
}
pub async fn setup(config: Arc<Config>) -> 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<Config>, lsp_server: LSPServerSocket) -> Self {
+ pub fn new(config: Arc<Config>, lsp_server: LSPServer) -> Self {
Self {
config,
lsp_server,
@@ -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