From 7397b826660ba24a1827c41e7e930603b32fb28a Mon Sep 17 00:00:00 2001 From: Victor Roemer Date: Sun, 8 Mar 2026 02:15:02 +0000 Subject: [PATCH 1/3] Fix lsp_servers[].file_patterns Problem While using `mcpls` in a C++ project I noticed that it always return the following error when calling get references from a '.h' header file: Error: tool call error: tool call failed for `mcpls/get_references` Caused by: tools/call failed: Mcp error: -32603: no LSP server configured for language: c After reviewing the documentation and creating an mcpls.toml with the following, it continued to not work [[lsp_servers]] language_id = "cpp" command = "clangd" args = ["--background-index", "--clang-tidy"] file_patterns = ["**/*.cpp", "**/*.cc", "**/*.cxx", "**/*.hpp", "**/*.c", "**/*.h"] # and/or with [[lsp_servers]] language_id = "c" command = "clangd" args = ["--background-index", "--clang-tidy"] file_patterns = ["**/*.c", "**/*.h"] With the above changing resulting in the language detection for matching file patterns to "plaintext" Error: tool call error: tool call failed for `mcpls/get_references` Caused by: tools/call failed: Mcp error: -32603: no LSP server configured for language: plaintext What changed - serve() now initializes the translator with an effective extension map built from both workspace mappings and LSP server file patterns: - crates/mcpls-core/src/lib.rs:114:114 - Added ServerConfig::build_effective_extension_map() to overlay extensions inferred from file_patterns: - crates/mcpls-core/src/config/mod.rs:287:287 - Added a small parser for simple glob extensions (e.g. **/*.h, *.c): - crates/mcpls-core/src/config/mod.rs:123:123 Tests added - Pattern-derived mapping overrides default extension mapping (.c/.h -> cpp): - crates/mcpls-core/src/config/mod.rs:700:700 - Complex non-simple patterns are ignored safely: - crates/mcpls-core/src/config/mod.rs:721:721 Verification - New tests passed. - Full mcpls-core unit/integration tests passed. - Existing unrelated doctest failure remains in lsp/types.rs (pre-existing visibility issue). Fix was implemented by Codex --- crates/mcpls-core/src/config/mod.rs | 82 +++++++++++++++++++++++++++++ crates/mcpls-core/src/lib.rs | 2 +- 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/crates/mcpls-core/src/config/mod.rs b/crates/mcpls-core/src/config/mod.rs index d1ac411..c6e06cf 100644 --- a/crates/mcpls-core/src/config/mod.rs +++ b/crates/mcpls-core/src/config/mod.rs @@ -116,6 +116,27 @@ impl WorkspaceConfig { } } +/// Extract a file extension from a glob-like file pattern. +/// +/// Supports common patterns such as `**/*.rs` and `*.h`. +/// Returns `None` for patterns without a simple trailing extension. +fn extract_extension_from_pattern(pattern: &str) -> Option { + let (_, ext) = pattern.rsplit_once('.')?; + if ext.is_empty() { + return None; + } + + // Keep this conservative: only accept plain extension-like tokens. + if ext + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') + { + Some(ext.to_string()) + } else { + None + } +} + fn default_position_encodings() -> Vec { vec!["utf-8".to_string(), "utf-16".to_string()] } @@ -258,6 +279,25 @@ fn default_language_extensions() -> Vec { } impl ServerConfig { + /// Build the effective extension map used for language detection. + /// + /// Starts with workspace mappings and overlays mappings inferred from + /// configured LSP server `file_patterns`. + #[must_use] + pub fn build_effective_extension_map(&self) -> HashMap { + let mut map = self.workspace.build_extension_map(); + + for server in &self.lsp_servers { + for pattern in &server.file_patterns { + if let Some(ext) = extract_extension_from_pattern(pattern) { + map.insert(ext, server.language_id.clone()); + } + } + } + + map + } + /// Load configuration from the default path. /// /// Default paths checked in order: @@ -656,6 +696,48 @@ mod tests { assert_eq!(map.get("unknown"), None); } + #[test] + fn test_build_effective_extension_map_overrides_with_file_patterns() { + let config = ServerConfig { + workspace: WorkspaceConfig::default(), + lsp_servers: vec![LspServerConfig { + language_id: "cpp".to_string(), + command: "clangd".to_string(), + args: vec![], + env: HashMap::new(), + file_patterns: vec!["**/*.c".to_string(), "**/*.h".to_string()], + initialization_options: None, + timeout_seconds: 30, + heuristics: None, + }], + }; + + let map = config.build_effective_extension_map(); + assert_eq!(map.get("c"), Some(&"cpp".to_string())); + assert_eq!(map.get("h"), Some(&"cpp".to_string())); + } + + #[test] + fn test_build_effective_extension_map_ignores_complex_patterns_without_extension() { + let config = ServerConfig { + workspace: WorkspaceConfig::default(), + lsp_servers: vec![LspServerConfig { + language_id: "cpp".to_string(), + command: "clangd".to_string(), + args: vec![], + env: HashMap::new(), + file_patterns: vec!["**/*".to_string(), "**/*.{h,hpp}".to_string()], + initialization_options: None, + timeout_seconds: 30, + heuristics: None, + }], + }; + + let map = config.build_effective_extension_map(); + // Default C/C++ mappings remain unchanged when patterns cannot be parsed. + assert_eq!(map.get("h"), Some(&"c".to_string())); + } + #[test] fn test_get_language_for_extension() { let workspace = WorkspaceConfig { diff --git a/crates/mcpls-core/src/lib.rs b/crates/mcpls-core/src/lib.rs index a0e4ec7..edea924 100644 --- a/crates/mcpls-core/src/lib.rs +++ b/crates/mcpls-core/src/lib.rs @@ -111,7 +111,7 @@ pub async fn serve(config: ServerConfig) -> Result<(), Error> { info!("Starting MCPLS server..."); let workspace_roots = resolve_workspace_roots(&config.workspace.roots); - let extension_map = config.workspace.build_extension_map(); + let extension_map = config.build_effective_extension_map(); let max_depth = Some(config.workspace.heuristics_max_depth); let mut translator = Translator::new().with_extensions(extension_map); From 39057dac9f393167894f93e04ab845792b246327 Mon Sep 17 00:00:00 2001 From: Victor Roemer Date: Sat, 21 Mar 2026 00:24:13 +0000 Subject: [PATCH 2/3] fix(config): polish file pattern extension mapping Add direct unit coverage for extract_extension_from_pattern edge cases, including empty input, no-dot patterns, dotfiles, and multi-dot filenames. Tighten the parser to reject dotfile basenames so hidden files like .gitignore do not get treated as language extensions. Update the translator initialization test to assert against build_effective_extension_map(), which matches the runtime code path introduced by the earlier file_patterns fix. Document the resulting C/C++ language detection behavior change in CHANGELOG.md. --- CHANGELOG.md | 3 +++ crates/mcpls-core/src/bridge/translator.rs | 2 +- crates/mcpls-core/src/config/mod.rs | 28 ++++++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6fed43..55e5bcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Changed + +- **C/C++ file pattern language detection** - When `lsp_servers[].file_patterns` include simple extensions such as `**/*.c` or `**/*.h`, mcpls now derives extension-to-language mappings from those patterns and overlays them onto the workspace extension map. This changes the default behavior for matching C/C++ files to prefer the configured LSP server language instead of falling back to built-in mappings or `plaintext`. ## [0.3.5] - 2026-03-17 diff --git a/crates/mcpls-core/src/bridge/translator.rs b/crates/mcpls-core/src/bridge/translator.rs index b853472..a9e0539 100644 --- a/crates/mcpls-core/src/bridge/translator.rs +++ b/crates/mcpls-core/src/bridge/translator.rs @@ -2776,7 +2776,7 @@ mod tests { lsp_servers: vec![], }; - let extension_map = config.workspace.build_extension_map(); + let extension_map = config.build_effective_extension_map(); assert_eq!(extension_map.get("nu"), Some(&"nushell".to_string())); assert_eq!(extension_map.get("rs"), Some(&"rust".to_string())); diff --git a/crates/mcpls-core/src/config/mod.rs b/crates/mcpls-core/src/config/mod.rs index c6e06cf..04cadcf 100644 --- a/crates/mcpls-core/src/config/mod.rs +++ b/crates/mcpls-core/src/config/mod.rs @@ -121,6 +121,11 @@ impl WorkspaceConfig { /// Supports common patterns such as `**/*.rs` and `*.h`. /// Returns `None` for patterns without a simple trailing extension. fn extract_extension_from_pattern(pattern: &str) -> Option { + let basename = pattern.rsplit('/').next().unwrap_or(pattern); + if basename.starts_with('.') { + return None; + } + let (_, ext) = pattern.rsplit_once('.')?; if ext.is_empty() { return None; @@ -696,6 +701,29 @@ mod tests { assert_eq!(map.get("unknown"), None); } + #[test] + fn test_extract_extension_from_pattern_empty_string() { + assert_eq!(extract_extension_from_pattern(""), None); + } + + #[test] + fn test_extract_extension_from_pattern_without_dot() { + assert_eq!(extract_extension_from_pattern("**/*"), None); + } + + #[test] + fn test_extract_extension_from_pattern_dotfile() { + assert_eq!(extract_extension_from_pattern(".gitignore"), None); + } + + #[test] + fn test_extract_extension_from_pattern_multi_dot_extension() { + assert_eq!( + extract_extension_from_pattern("foo.tar.gz"), + Some("gz".to_string()) + ); + } + #[test] fn test_build_effective_extension_map_overrides_with_file_patterns() { let config = ServerConfig { From edf64c697f9bcf664e938675efbff92c5000ee24 Mon Sep 17 00:00:00 2001 From: Victor Roemer Date: Wed, 1 Apr 2026 07:41:13 +0000 Subject: [PATCH 3/3] fix(diagnostics): support published diagnostics fallback Make get_diagnostics capability-aware so servers without pull diagnostics support fall back to cached published diagnostics instead of surfacing -32601. Wire LSP notification forwarding into normal server startup so publishDiagnostics, log messages, and showMessage notifications populate the translator cache during serve(). Add focused tests for notification ingestion and cached diagnostics fallback, and clarify the MCP tool description to reflect the new behavior. --- crates/mcpls-core/src/bridge/translator.rs | 210 ++++++++++++++++++++- crates/mcpls-core/src/lib.rs | 30 ++- crates/mcpls-core/src/lsp/lifecycle.rs | 54 +++++- crates/mcpls-core/src/lsp/mod.rs | 5 +- crates/mcpls-core/src/mcp/server.rs | 2 +- 5 files changed, 281 insertions(+), 20 deletions(-) diff --git a/crates/mcpls-core/src/bridge/translator.rs b/crates/mcpls-core/src/bridge/translator.rs index a9e0539..5a6eb26 100644 --- a/crates/mcpls-core/src/bridge/translator.rs +++ b/crates/mcpls-core/src/bridge/translator.rs @@ -18,10 +18,10 @@ use tokio::time::Duration; use url::Url; use super::state::{ResourceLimits, detect_language}; -use super::{DocumentTracker, NotificationCache}; +use super::{DocumentTracker, LogLevel, MessageType, NotificationCache}; use crate::bridge::encoding::mcp_to_lsp_position; use crate::error::{Error, Result}; -use crate::lsp::{LspClient, LspServer}; +use crate::lsp::{LspClient, LspNotification, LspServer}; /// Translator handles MCP tool calls by converting them to LSP requests. #[derive(Debug)] @@ -103,6 +103,28 @@ impl Translator { &mut self.notification_cache } + /// Store a forwarded LSP notification in the local cache. + pub fn handle_notification(&mut self, notification: LspNotification) { + match notification { + LspNotification::PublishDiagnostics(params) => { + self.notification_cache.store_diagnostics( + ¶ms.uri, + params.version, + params.diagnostics, + ); + } + LspNotification::LogMessage(params) => { + self.notification_cache + .store_log(LogLevel::from(params.typ), params.message); + } + LspNotification::ShowMessage(params) => { + self.notification_cache + .store_message(MessageType::from(params.typ), params.message); + } + LspNotification::Other { .. } => {} + } + } + // TODO: These methods will be implemented in Phase 3-5 // Initialize and shutdown are now handled by LspServer in lifecycle.rs @@ -453,13 +475,27 @@ impl Translator { /// Get a cloned LSP client for a file path based on language detection. fn get_client_for_file(&self, path: &Path) -> Result { - let language_id = detect_language(path, &self.extension_map); + let language_id = self.get_language_id_for_file(path); self.lsp_clients .get(&language_id) .cloned() .ok_or(Error::NoServerForLanguage(language_id)) } + /// Resolve the language ID for a file path. + fn get_language_id_for_file(&self, path: &Path) -> String { + detect_language(path, &self.extension_map) + } + + /// Determine if the selected server for a file supports pull diagnostics. + fn supports_pull_diagnostics(&self, path: &Path) -> bool { + let language_id = self.get_language_id_for_file(path); + self.lsp_servers + .get(&language_id) + .and_then(|server| server.capabilities().diagnostic_provider.as_ref()) + .is_some() + } + /// Parse and validate a file URI, returning the validated path. /// /// # Errors @@ -668,13 +704,18 @@ impl Translator { let path = PathBuf::from(&file_path); let validated_path = self.validate_path(&path)?; let client = self.get_client_for_file(&validated_path)?; - let uri = self + let supports_pull_diagnostics = self.supports_pull_diagnostics(&validated_path); + let _uri = self .document_tracker .ensure_open(&validated_path, &client) .await?; + if !supports_pull_diagnostics { + return self.handle_cached_diagnostics(&file_path); + } + let params = lsp_types::DocumentDiagnosticParams { - text_document: TextDocumentIdentifier { uri }, + text_document: TextDocumentIdentifier { uri: _uri }, identifier: None, previous_result_id: None, work_done_progress_params: WorkDoneProgressParams::default(), @@ -682,9 +723,16 @@ impl Translator { }; let timeout_duration = Duration::from_secs(30); - let response: lsp_types::DocumentDiagnosticReportResult = client + let response: lsp_types::DocumentDiagnosticReportResult = match client .request("textDocument/diagnostic", params, timeout_duration) - .await?; + .await + { + Ok(response) => response, + Err(Error::LspServerError { code: -32601, .. }) => { + return self.handle_cached_diagnostics(&file_path); + } + Err(error) => return Err(error), + }; let diagnostics = match response { lsp_types::DocumentDiagnosticReportResult::Report(report) => match report { @@ -1577,11 +1625,19 @@ fn convert_code_action(action: lsp_types::CodeAction) -> CodeAction { #[allow(clippy::unwrap_used)] mod tests { use std::fs; + use std::process::Stdio; + use lsp_types::{ + Diagnostic as LspDiagnostic, DiagnosticSeverity as LspDiagnosticSeverity, Position, + PositionEncodingKind, PublishDiagnosticsParams, Range as LspRange, ServerCapabilities, Uri, + }; use tempfile::TempDir; + use tokio::process::Command; use url::Url; use super::*; + use crate::config::LspServerConfig; + use crate::lsp::LspTransport; #[test] fn test_translator_new() { @@ -2158,6 +2214,146 @@ mod tests { assert_eq!(diags.diagnostics.len(), 0); } + fn create_test_client(language_id: &str) -> LspClient { + let mock_stdin = Command::new("cat") + .stdin(Stdio::piped()) + .spawn() + .unwrap() + .stdin + .take() + .unwrap(); + + let mock_stdout = Command::new("tail") + .arg("-f") + .arg("/dev/null") + .stdout(Stdio::piped()) + .spawn() + .unwrap() + .stdout + .take() + .unwrap(); + + let transport = LspTransport::new(mock_stdin, mock_stdout); + let mut config = LspServerConfig::clangd(); + config.language_id = language_id.to_string(); + LspClient::from_transport(config, transport) + } + + #[test] + fn test_handle_notification_stores_diagnostics() { + let mut translator = Translator::new(); + let uri: Uri = "file:///workspace/test.cpp".parse().unwrap(); + let diagnostic = LspDiagnostic { + range: LspRange { + start: Position { + line: 0, + character: 0, + }, + end: Position { + line: 0, + character: 3, + }, + }, + severity: Some(LspDiagnosticSeverity::ERROR), + message: "test diagnostic".to_string(), + ..Default::default() + }; + + translator.handle_notification(LspNotification::PublishDiagnostics( + PublishDiagnosticsParams { + uri: uri.clone(), + version: Some(1), + diagnostics: vec![diagnostic], + }, + )); + + let cached = translator.notification_cache().get_diagnostics(uri.as_str()).unwrap(); + assert_eq!(cached.diagnostics.len(), 1); + assert_eq!(cached.version, Some(1)); + assert_eq!(cached.diagnostics[0].message, "test diagnostic"); + } + + #[test] + fn test_handle_notification_stores_logs_and_messages() { + let mut translator = Translator::new(); + + translator.handle_notification(LspNotification::LogMessage(lsp_types::LogMessageParams { + typ: lsp_types::MessageType::WARNING, + message: "log entry".to_string(), + })); + translator.handle_notification(LspNotification::ShowMessage( + lsp_types::ShowMessageParams { + typ: lsp_types::MessageType::INFO, + message: "user message".to_string(), + }, + )); + + let logs = translator.notification_cache().get_logs(); + assert_eq!(logs.len(), 1); + assert_eq!(logs[0].level, LogLevel::Warning); + assert_eq!(logs[0].message, "log entry"); + + let messages = translator.notification_cache().get_messages(); + assert_eq!(messages.len(), 1); + assert_eq!(messages[0].message_type, MessageType::Info); + assert_eq!(messages[0].message, "user message"); + } + + #[tokio::test] + async fn test_handle_diagnostics_falls_back_to_cached_when_pull_unsupported() { + let temp_dir = TempDir::new().unwrap(); + let test_file = temp_dir.path().join("test.cpp"); + fs::write(&test_file, "int main() { return 0; }\n").unwrap(); + + let client = create_test_client("cpp"); + let server = LspServer::new_for_tests( + client.clone(), + ServerCapabilities::default(), + PositionEncodingKind::UTF8, + ); + + let mut extension_map = HashMap::new(); + extension_map.insert("cpp".to_string(), "cpp".to_string()); + + let mut translator = Translator::new().with_extensions(extension_map); + translator.set_workspace_roots(vec![temp_dir.path().to_path_buf()]); + translator.register_client("cpp".to_string(), client); + translator.register_server("cpp".to_string(), server); + + let uri = Url::from_file_path(&test_file).unwrap(); + let diagnostic = LspDiagnostic { + range: LspRange { + start: Position { + line: 0, + character: 4, + }, + end: Position { + line: 0, + character: 8, + }, + }, + severity: Some(LspDiagnosticSeverity::WARNING), + message: "cached diagnostic".to_string(), + ..Default::default() + }; + let cached_uri: Uri = uri.as_str().parse().unwrap(); + translator + .notification_cache_mut() + .store_diagnostics(&cached_uri, Some(1), vec![diagnostic]); + + let result = translator + .handle_diagnostics(test_file.to_str().unwrap().to_string()) + .await + .unwrap(); + + assert_eq!(result.diagnostics.len(), 1); + assert_eq!(result.diagnostics[0].message, "cached diagnostic"); + assert!(matches!( + result.diagnostics[0].severity, + DiagnosticSeverity::Warning + )); + } + #[test] fn test_handle_server_logs_with_filter() { use crate::bridge::notifications::LogLevel; diff --git a/crates/mcpls-core/src/lib.rs b/crates/mcpls-core/src/lib.rs index edea924..37d8076 100644 --- a/crates/mcpls-core/src/lib.rs +++ b/crates/mcpls-core/src/lib.rs @@ -39,9 +39,9 @@ use std::sync::Arc; use bridge::Translator; pub use config::ServerConfig; pub use error::Error; -use lsp::{LspServer, ServerInitConfig}; +use lsp::{LspNotification, LspServer, ServerInitConfig}; use rmcp::ServiceExt; -use tokio::sync::Mutex; +use tokio::sync::{Mutex, mpsc}; use tracing::{error, info, warn}; /// Resolve workspace roots from config or current directory. @@ -116,6 +116,16 @@ pub async fn serve(config: ServerConfig) -> Result<(), Error> { let mut translator = Translator::new().with_extensions(extension_map); translator.set_workspace_roots(workspace_roots.clone()); + let translator = Arc::new(Mutex::new(translator)); + + let (notification_tx, mut notification_rx) = mpsc::channel::(256); + let notification_translator = Arc::clone(&translator); + tokio::spawn(async move { + while let Some(notification) = notification_rx.recv().await { + let mut translator = notification_translator.lock().await; + translator.handle_notification(notification); + } + }); // Build configurations for batch spawning with heuristics filtering let applicable_configs: Vec = config @@ -148,7 +158,8 @@ pub async fn serve(config: ServerConfig) -> Result<(), Error> { ); // Spawn all servers with graceful degradation - let result = LspServer::spawn_batch(&applicable_configs).await; + let result = + LspServer::spawn_batch_with_notifications(&applicable_configs, Some(notification_tx)).await; // Handle the three possible outcomes if result.all_failed() { @@ -178,16 +189,17 @@ pub async fn serve(config: ServerConfig) -> Result<(), Error> { // Register all successfully initialized servers let server_count = result.server_count(); - for (language_id, server) in result.servers { - let client = server.client().clone(); - translator.register_client(language_id.clone(), client); - translator.register_server(language_id.clone(), server); + { + let mut translator = translator.lock().await; + for (language_id, server) in result.servers { + let client = server.client().clone(); + translator.register_client(language_id.clone(), client); + translator.register_server(language_id.clone(), server); + } } info!("Proceeding with {} LSP server(s)", server_count); - let translator = Arc::new(Mutex::new(translator)); - info!("Starting MCP server with rmcp..."); let mcp_server = mcp::McplsServer::new(translator); diff --git a/crates/mcpls-core/src/lsp/lifecycle.rs b/crates/mcpls-core/src/lsp/lifecycle.rs index fcef85b..00803db 100644 --- a/crates/mcpls-core/src/lsp/lifecycle.rs +++ b/crates/mcpls-core/src/lsp/lifecycle.rs @@ -16,6 +16,7 @@ use lsp_types::{ ClientCapabilities, ClientInfo, GeneralClientCapabilities, InitializeParams, InitializeResult, InitializedParams, PositionEncodingKind, ServerCapabilities, Uri, WorkspaceFolder, }; +use tokio::sync::mpsc; use tokio::process::Command; use tokio::time::Duration; use tracing::{debug, info}; @@ -24,6 +25,7 @@ use crate::config::LspServerConfig; use crate::error::{Error, Result, ServerSpawnFailure}; use crate::lsp::client::LspClient; use crate::lsp::transport::LspTransport; +use crate::lsp::types::LspNotification; /// State of an LSP server connection. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -199,6 +201,17 @@ impl LspServer { /// - Initialize request fails or times out /// - Server returns error during initialization pub async fn spawn(config: ServerInitConfig) -> Result { + Self::spawn_with_notifications(config, None).await + } + + /// Spawn and initialize LSP server with optional notification forwarding. + /// + /// When a notification sender is provided, server notifications are + /// forwarded to the caller for caching or further processing. + pub async fn spawn_with_notifications( + config: ServerInitConfig, + notification_tx: Option>, + ) -> Result { info!( "Spawning LSP server: {} {:?}", config.server_config.command, config.server_config.args @@ -226,7 +239,14 @@ impl LspServer { .ok_or_else(|| Error::Transport("Failed to capture stdout".to_string()))?; let transport = LspTransport::new(stdin, stdout); - let client = LspClient::from_transport(config.server_config.clone(), transport); + let client = match notification_tx { + Some(notification_tx) => LspClient::from_transport_with_notifications( + config.server_config.clone(), + transport, + notification_tx, + ), + None => LspClient::from_transport(config.server_config.clone(), transport), + }; let (capabilities, position_encoding) = Self::initialize(&client, &config).await?; @@ -367,6 +387,28 @@ impl LspServer { &self.client } + /// Create an `LspServer` for unit tests without full LSP initialization. + #[cfg(test)] + pub(crate) fn new_for_tests( + client: LspClient, + capabilities: ServerCapabilities, + position_encoding: PositionEncodingKind, + ) -> Self { + let child = Command::new("cat") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .kill_on_drop(true) + .spawn() + .expect("failed to spawn test child process"); + + Self { + client, + capabilities, + position_encoding, + _child: child, + } + } + /// Shutdown server gracefully. /// /// Sends shutdown request, waits for response, then sends exit notification. @@ -437,13 +479,21 @@ impl LspServer { /// # } /// ``` pub async fn spawn_batch(configs: &[ServerInitConfig]) -> ServerInitResult { + Self::spawn_batch_with_notifications(configs, None).await + } + + /// Spawn multiple LSP servers with optional notification forwarding. + pub async fn spawn_batch_with_notifications( + configs: &[ServerInitConfig], + notification_tx: Option>, + ) -> ServerInitResult { let mut result = ServerInitResult::new(); for config in configs { let language_id = config.server_config.language_id.clone(); let command = config.server_config.command.clone(); - match Self::spawn(config.clone()).await { + match Self::spawn_with_notifications(config.clone(), notification_tx.clone()).await { Ok(server) => { info!( "Successfully spawned LSP server: {} ({})", diff --git a/crates/mcpls-core/src/lsp/mod.rs b/crates/mcpls-core/src/lsp/mod.rs index ccc8585..84e2cf0 100644 --- a/crates/mcpls-core/src/lsp/mod.rs +++ b/crates/mcpls-core/src/lsp/mod.rs @@ -11,4 +11,7 @@ mod types; pub use client::LspClient; pub use lifecycle::{LspServer, ServerInitConfig, ServerInitResult, ServerState}; pub use transport::LspTransport; -pub use types::{InboundMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, RequestId}; +pub use types::{ + InboundMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, LspNotification, + RequestId, +}; diff --git a/crates/mcpls-core/src/mcp/server.rs b/crates/mcpls-core/src/mcp/server.rs index 861821d..6028c76 100644 --- a/crates/mcpls-core/src/mcp/server.rs +++ b/crates/mcpls-core/src/mcp/server.rs @@ -117,7 +117,7 @@ impl McplsServer { /// Get diagnostics for a file. #[tool( - description = "Diagnostics for a file. Returns errors, warnings, and hints with severity and location." + description = "Diagnostics for a file. Uses pull diagnostics when the server supports them, otherwise falls back to cached published diagnostics." )] async fn get_diagnostics( &self,