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