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::{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) => match cwd.canonicalize() {
61 Ok(canonical) => {
62 info!(
63 "Using current directory as workspace root: {}",
64 canonical.display()
65 );
66 vec![canonical]
67 }
68 Err(e) => {
69 warn!("Failed to canonicalize current directory: {e}");
70 vec![PathBuf::from(".")]
71 }
72 },
73 Err(e) => {
74 warn!("Failed to get current directory: {e}");
75 vec![PathBuf::from(".")]
76 }
77 }
78 } else {
79 config_roots.to_vec()
80 }
81}
82
83pub async fn serve(config: ServerConfig) -> Result<(), Error> {
94 tracing::info!("Starting MCPLS server...");
95
96 let mut translator = Translator::new();
97 let workspace_roots = resolve_workspace_roots(&config.workspace.roots);
98
99 translator.set_workspace_roots(workspace_roots.clone());
100
101 for lsp_config in config.lsp_servers {
102 tracing::info!(
103 "Spawning LSP server for language '{}': {} {:?}",
104 lsp_config.language_id,
105 lsp_config.command,
106 lsp_config.args
107 );
108
109 let server_init_config = ServerInitConfig {
110 server_config: lsp_config.clone(),
111 workspace_roots: workspace_roots.clone(),
112 initialization_options: lsp_config.initialization_options.clone(),
113 };
114
115 let server = LspServer::spawn(server_init_config).await?;
116 let client = server.client().clone();
117
118 translator.register_client(lsp_config.language_id.clone(), client);
119 translator.register_server(lsp_config.language_id.clone(), server);
120 }
121
122 let translator = Arc::new(Mutex::new(translator));
123
124 tracing::info!("Starting MCP server with rmcp...");
125 let mcp_server = mcp::McplsServer::new(translator);
126
127 tracing::info!("MCPLS server initialized successfully");
128 tracing::info!("Listening for MCP requests on stdio...");
129
130 let service = mcp_server
131 .serve(rmcp::transport::stdio())
132 .await
133 .map_err(|e| Error::McpServer(format!("Failed to start MCP server: {e}")))?;
134
135 service
136 .waiting()
137 .await
138 .map_err(|e| Error::McpServer(format!("MCP server error: {e}")))?;
139
140 tracing::info!("MCPLS server shutting down");
141 Ok(())
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn test_resolve_workspace_roots_empty_config() {
150 let roots = resolve_workspace_roots(&[]);
151 assert_eq!(roots.len(), 1);
152 assert!(
153 roots[0].is_absolute(),
154 "Workspace root should be absolute path"
155 );
156 }
157
158 #[test]
159 fn test_resolve_workspace_roots_with_config() {
160 let config_roots = vec![PathBuf::from("/test/root")];
161 let roots = resolve_workspace_roots(&config_roots);
162 assert_eq!(roots, config_roots);
163 }
164
165 #[test]
166 fn test_resolve_workspace_roots_multiple_paths() {
167 let config_roots = vec![PathBuf::from("/test/root1"), PathBuf::from("/test/root2")];
168 let roots = resolve_workspace_roots(&config_roots);
169 assert_eq!(roots, config_roots);
170 assert_eq!(roots.len(), 2);
171 }
172}