mcpls_core/
lib.rs

1//! # mcpls-core
2//!
3//! Core library for MCP (Model Context Protocol) to LSP (Language Server Protocol) translation.
4//!
5//! This crate provides the fundamental building blocks for bridging AI agents with
6//! language servers, enabling semantic code intelligence through MCP tools.
7//!
8//! ## Architecture
9//!
10//! The library is organized into several modules:
11//!
12//! - [`lsp`] - LSP client implementation for communicating with language servers
13//! - [`mcp`] - MCP tool definitions and handlers
14//! - [`bridge`] - Translation layer between MCP and LSP protocols
15//! - [`config`] - Configuration types and loading
16//! - [`error`] - Error types for the library
17//!
18//! ## Example
19//!
20//! ```rust,ignore
21//! use mcpls_core::{serve, ServerConfig};
22//!
23//! #[tokio::main]
24//! async fn main() -> Result<(), mcpls_core::Error> {
25//!     let config = ServerConfig::load()?;
26//!     serve(config).await
27//! }
28//! ```
29
30pub 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
47/// Resolve workspace roots from config or current directory.
48///
49/// If no workspace roots are provided in the configuration, this function
50/// will use the current working directory, canonicalized for security.
51///
52/// # Returns
53///
54/// A vector of workspace root paths. If config roots are provided, they are
55/// returned as-is. Otherwise, returns the canonicalized current directory,
56/// falling back to relative "." if canonicalization fails.
57fn 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
83/// Start the MCPLS server with the given configuration.
84///
85/// This is the primary entry point for running the MCP-LSP bridge.
86///
87/// # Errors
88///
89/// Returns an error if:
90/// - LSP server initialization fails
91/// - MCP server setup fails
92/// - Configuration is invalid
93pub 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}