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