From 152e28f4d81b41225d9f6cbea616a52698797ff4 Mon Sep 17 00:00:00 2001 From: Victor Roemer Date: Sun, 8 Mar 2026 02:15:02 +0000 Subject: [PATCH 1/2] 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 cb28dc26cb766da837d322924e85815c1470fdd8 Mon Sep 17 00:00:00 2001 From: Victor Roemer Date: Sat, 21 Mar 2026 00:24:13 +0000 Subject: [PATCH 2/2] 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 {