1pub mod bridge;
31pub mod config;
32pub mod error;
33pub mod lsp;
34pub mod mcp;
35
36use std::path::PathBuf;
37use std::sync::Arc;
38
39use bridge::Translator;
40pub use config::ServerConfig;
41pub use error::Error;
42use lsp::{LspServer, ServerInitConfig};
43use rmcp::ServiceExt;
44use tokio::sync::Mutex;
45use tracing::{error, info, warn};
46
47fn resolve_workspace_roots(config_roots: &[PathBuf]) -> Vec<PathBuf> {
58 if config_roots.is_empty() {
59 match std::env::current_dir() {
60 Ok(cwd) => {
61 match cwd.canonicalize() {
63 Ok(canonical) => {
64 info!(
65 "Using current directory as workspace root: {}",
66 canonical.display()
67 );
68 vec![canonical]
69 }
70 Err(e) => {
71 warn!(
74 "Failed to canonicalize current directory: {e}, using non-canonical path"
75 );
76 vec![cwd]
77 }
78 }
79 }
80 Err(e) => {
81 warn!("Failed to get current directory: {e}, using fallback");
84 vec![PathBuf::from(".")]
85 }
86 }
87 } else {
88 config_roots.to_vec()
89 }
90}
91
92pub async fn serve(config: ServerConfig) -> Result<(), Error> {
111 info!("Starting MCPLS server...");
112
113 let workspace_roots = resolve_workspace_roots(&config.workspace.roots);
114 let extension_map = config.workspace.build_extension_map();
115 let max_depth = Some(config.workspace.heuristics_max_depth);
116
117 let mut translator = Translator::new().with_extensions(extension_map);
118 translator.set_workspace_roots(workspace_roots.clone());
119
120 let applicable_configs: Vec<ServerInitConfig> = config
122 .lsp_servers
123 .iter()
124 .filter_map(|lsp_config| {
125 let should_spawn = workspace_roots
126 .iter()
127 .any(|root| lsp_config.should_spawn(root, max_depth));
128
129 if !should_spawn {
130 info!(
131 "Skipping LSP server '{}' ({}): no project markers found",
132 lsp_config.language_id, lsp_config.command
133 );
134 return None;
135 }
136
137 Some(ServerInitConfig {
138 server_config: lsp_config.clone(),
139 workspace_roots: workspace_roots.clone(),
140 initialization_options: lsp_config.initialization_options.clone(),
141 })
142 })
143 .collect();
144
145 info!(
146 "Attempting to spawn {} applicable LSP server(s)...",
147 applicable_configs.len()
148 );
149
150 let result = LspServer::spawn_batch(&applicable_configs).await;
152
153 if result.all_failed() {
155 return Err(Error::AllServersFailedToInit {
156 count: result.failure_count(),
157 failures: result.failures,
158 });
159 }
160
161 if result.partial_success() {
162 warn!(
163 "Partial server initialization: {} succeeded, {} failed",
164 result.server_count(),
165 result.failure_count()
166 );
167 for failure in &result.failures {
168 error!("Server initialization failed: {}", failure);
169 }
170 }
171
172 if !result.has_servers() {
174 return Err(Error::NoServersAvailable(
175 "none configured or all failed to initialize".to_string(),
176 ));
177 }
178
179 let server_count = result.server_count();
181 for (language_id, server) in result.servers {
182 let client = server.client().clone();
183 translator.register_client(language_id.clone(), client);
184 translator.register_server(language_id.clone(), server);
185 }
186
187 info!("Proceeding with {} LSP server(s)", server_count);
188
189 let translator = Arc::new(Mutex::new(translator));
190
191 info!("Starting MCP server with rmcp...");
192 let mcp_server = mcp::McplsServer::new(translator);
193
194 info!("MCPLS server initialized successfully");
195 info!("Listening for MCP requests on stdio...");
196
197 let service = mcp_server
198 .serve(rmcp::transport::stdio())
199 .await
200 .map_err(|e| Error::McpServer(format!("Failed to start MCP server: {e}")))?;
201
202 service
203 .waiting()
204 .await
205 .map_err(|e| Error::McpServer(format!("MCP server error: {e}")))?;
206
207 info!("MCPLS server shutting down");
208 Ok(())
209}
210
211#[cfg(test)]
212#[allow(clippy::unwrap_used)]
213mod tests {
214 use super::*;
215
216 #[test]
217 fn test_resolve_workspace_roots_empty_config() {
218 let roots = resolve_workspace_roots(&[]);
219 assert_eq!(roots.len(), 1);
220 assert!(
221 roots[0].is_absolute(),
222 "Workspace root should be absolute path"
223 );
224 }
225
226 #[test]
227 fn test_resolve_workspace_roots_with_config() {
228 let config_roots = vec![PathBuf::from("/test/root")];
229 let roots = resolve_workspace_roots(&config_roots);
230 assert_eq!(roots, config_roots);
231 }
232
233 #[test]
234 fn test_resolve_workspace_roots_multiple_paths() {
235 let config_roots = vec![PathBuf::from("/test/root1"), PathBuf::from("/test/root2")];
236 let roots = resolve_workspace_roots(&config_roots);
237 assert_eq!(roots, config_roots);
238 assert_eq!(roots.len(), 2);
239 }
240
241 #[test]
242 fn test_resolve_workspace_roots_preserves_order() {
243 let config_roots = vec![
244 PathBuf::from("/workspace/alpha"),
245 PathBuf::from("/workspace/beta"),
246 PathBuf::from("/workspace/gamma"),
247 ];
248 let roots = resolve_workspace_roots(&config_roots);
249 assert_eq!(roots[0], PathBuf::from("/workspace/alpha"));
250 assert_eq!(roots[1], PathBuf::from("/workspace/beta"));
251 assert_eq!(roots[2], PathBuf::from("/workspace/gamma"));
252 }
253
254 #[test]
255 fn test_resolve_workspace_roots_single_path() {
256 let config_roots = vec![PathBuf::from("/single/workspace")];
257 let roots = resolve_workspace_roots(&config_roots);
258 assert_eq!(roots.len(), 1);
259 assert_eq!(roots[0], PathBuf::from("/single/workspace"));
260 }
261
262 #[test]
263 fn test_resolve_workspace_roots_empty_returns_cwd() {
264 let roots = resolve_workspace_roots(&[]);
265 assert!(
266 !roots.is_empty(),
267 "Should return at least one workspace root"
268 );
269 }
270
271 #[test]
272 fn test_resolve_workspace_roots_relative_paths() {
273 let config_roots = vec![
274 PathBuf::from("relative/path1"),
275 PathBuf::from("relative/path2"),
276 ];
277 let roots = resolve_workspace_roots(&config_roots);
278 assert_eq!(roots.len(), 2);
279 assert_eq!(roots[0], PathBuf::from("relative/path1"));
280 assert_eq!(roots[1], PathBuf::from("relative/path2"));
281 }
282
283 #[test]
284 fn test_resolve_workspace_roots_mixed_paths() {
285 let config_roots = vec![
286 PathBuf::from("/absolute/path"),
287 PathBuf::from("relative/path"),
288 ];
289 let roots = resolve_workspace_roots(&config_roots);
290 assert_eq!(roots.len(), 2);
291 assert_eq!(roots[0], PathBuf::from("/absolute/path"));
292 assert_eq!(roots[1], PathBuf::from("relative/path"));
293 }
294
295 #[test]
296 fn test_resolve_workspace_roots_with_dot_path() {
297 let config_roots = vec![PathBuf::from(".")];
298 let roots = resolve_workspace_roots(&config_roots);
299 assert_eq!(roots, config_roots);
300 }
301
302 #[test]
303 fn test_resolve_workspace_roots_with_parent_path() {
304 let config_roots = vec![PathBuf::from("..")];
305 let roots = resolve_workspace_roots(&config_roots);
306 assert_eq!(roots.len(), 1);
307 assert_eq!(roots[0], PathBuf::from(".."));
308 }
309
310 #[test]
311 fn test_resolve_workspace_roots_unicode_paths() {
312 let config_roots = vec![
313 PathBuf::from("/workspace/テスト"),
314 PathBuf::from("/workspace/тест"),
315 ];
316 let roots = resolve_workspace_roots(&config_roots);
317 assert_eq!(roots.len(), 2);
318 assert_eq!(roots[0], PathBuf::from("/workspace/テスト"));
319 assert_eq!(roots[1], PathBuf::from("/workspace/тест"));
320 }
321
322 #[test]
323 fn test_resolve_workspace_roots_spaces_in_paths() {
324 let config_roots = vec![
325 PathBuf::from("/workspace/path with spaces"),
326 PathBuf::from("/another path/workspace"),
327 ];
328 let roots = resolve_workspace_roots(&config_roots);
329 assert_eq!(roots.len(), 2);
330 assert_eq!(roots[0], PathBuf::from("/workspace/path with spaces"));
331 }
332
333 mod graceful_degradation_tests {
335 use super::*;
336 use crate::error::ServerSpawnFailure;
337 use crate::lsp::ServerInitResult;
338
339 #[test]
340 fn test_all_servers_failed_error_handling() {
341 let mut result = ServerInitResult::new();
342 result.add_failure(ServerSpawnFailure {
343 language_id: "rust".to_string(),
344 command: "rust-analyzer".to_string(),
345 message: "not found".to_string(),
346 });
347 result.add_failure(ServerSpawnFailure {
348 language_id: "python".to_string(),
349 command: "pyright".to_string(),
350 message: "not found".to_string(),
351 });
352
353 assert!(result.all_failed());
354 assert_eq!(result.failure_count(), 2);
355 assert_eq!(result.server_count(), 0);
356 }
357
358 #[test]
359 fn test_partial_success_detection() {
360 use std::collections::HashMap;
361
362 let mut result = ServerInitResult::new();
363 result.servers = HashMap::new(); result.add_failure(ServerSpawnFailure {
366 language_id: "python".to_string(),
367 command: "pyright".to_string(),
368 message: "not found".to_string(),
369 });
370
371 assert_eq!(result.failure_count(), 1);
373 assert_eq!(result.server_count(), 0);
374 }
375
376 #[test]
377 fn test_all_servers_succeeded_detection() {
378 use std::collections::HashMap;
379
380 let mut result = ServerInitResult::new();
381 result.servers = HashMap::new(); assert_eq!(result.failure_count(), 0);
384 assert!(!result.all_failed());
385 assert!(!result.partial_success());
386 }
387
388 #[test]
389 fn test_all_servers_failed_to_init_error() {
390 let failures = vec![
391 ServerSpawnFailure {
392 language_id: "rust".to_string(),
393 command: "rust-analyzer".to_string(),
394 message: "command not found".to_string(),
395 },
396 ServerSpawnFailure {
397 language_id: "python".to_string(),
398 command: "pyright".to_string(),
399 message: "permission denied".to_string(),
400 },
401 ];
402
403 let err = Error::AllServersFailedToInit { count: 2, failures };
404
405 assert!(err.to_string().contains("all LSP servers failed"));
406 assert!(err.to_string().contains("2 configured"));
407
408 if let Error::AllServersFailedToInit { count, failures: f } = err {
410 assert_eq!(count, 2);
411 assert_eq!(f.len(), 2);
412 assert_eq!(f[0].language_id, "rust");
413 assert_eq!(f[1].language_id, "python");
414 } else {
415 panic!("Expected AllServersFailedToInit error");
416 }
417 }
418
419 #[test]
420 fn test_graceful_degradation_with_empty_config() {
421 let result = ServerInitResult::new();
422
423 assert!(!result.all_failed());
425 assert!(!result.partial_success());
426 assert!(!result.has_servers());
427 assert_eq!(result.server_count(), 0);
428 assert_eq!(result.failure_count(), 0);
429 }
430
431 #[test]
432 fn test_server_spawn_failure_display() {
433 let failure = ServerSpawnFailure {
434 language_id: "typescript".to_string(),
435 command: "tsserver".to_string(),
436 message: "executable not found in PATH".to_string(),
437 };
438
439 let display = failure.to_string();
440 assert!(display.contains("typescript"));
441 assert!(display.contains("tsserver"));
442 assert!(display.contains("executable not found"));
443 }
444
445 #[test]
446 fn test_result_helpers_consistency() {
447 let mut result = ServerInitResult::new();
448
449 assert!(!result.has_servers());
451 assert!(!result.all_failed());
452 assert!(!result.partial_success());
453
454 result.add_failure(ServerSpawnFailure {
456 language_id: "go".to_string(),
457 command: "gopls".to_string(),
458 message: "error".to_string(),
459 });
460
461 assert!(result.all_failed());
462 assert!(!result.has_servers());
463 assert!(!result.partial_success());
464 }
465
466 #[tokio::test]
467 async fn test_serve_fails_with_no_servers_available() {
468 use crate::config::{LspServerConfig, WorkspaceConfig};
469
470 let config = ServerConfig {
472 workspace: WorkspaceConfig {
473 roots: vec![PathBuf::from("/tmp/test-workspace")],
474 position_encodings: vec!["utf-8".to_string(), "utf-16".to_string()],
475 language_extensions: vec![],
476 heuristics_max_depth: 10,
477 },
478 lsp_servers: vec![LspServerConfig {
479 language_id: "rust".to_string(),
480 command: "nonexistent-command-that-will-fail-12345".to_string(),
481 args: vec![],
482 env: std::collections::HashMap::new(),
483 file_patterns: vec!["**/*.rs".to_string()],
484 initialization_options: None,
485 timeout_seconds: 10,
486 heuristics: None,
487 }],
488 };
489
490 let result = serve(config).await;
491
492 assert!(result.is_err());
493 let err = result.unwrap_err();
494
495 assert!(
498 matches!(err, Error::NoServersAvailable(_))
499 || matches!(err, Error::AllServersFailedToInit { .. }),
500 "Expected NoServersAvailable or AllServersFailedToInit error, got: {err:?}"
501 );
502 }
503
504 #[tokio::test]
505 async fn test_serve_fails_with_empty_config() {
506 use crate::config::WorkspaceConfig;
507
508 let config = ServerConfig {
510 workspace: WorkspaceConfig {
511 roots: vec![PathBuf::from("/tmp/test-workspace")],
512 position_encodings: vec!["utf-8".to_string(), "utf-16".to_string()],
513 language_extensions: vec![],
514 heuristics_max_depth: 10,
515 },
516 lsp_servers: vec![],
517 };
518
519 let result = serve(config).await;
520
521 assert!(result.is_err());
522 let err = result.unwrap_err();
523
524 assert!(
526 matches!(err, Error::NoServersAvailable(_)),
527 "Expected NoServersAvailable error, got: {err:?}"
528 );
529
530 if let Error::NoServersAvailable(msg) = err {
531 assert!(msg.contains("none configured"));
532 }
533 }
534 }
535}