diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ab4082..ce9505a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Documentation link** — Disambiguated `error` module link in crate docs (was causing `rustdoc::broken-intra-doc-links` warning) - **Test isolation** — Fixed test isolation issue in `test_load_does_not_overwrite_existing_config` by properly saving and restoring working directory, resolving llvm-cov coverage job failures +### Added + +- `get_server_status` — New MCP tool showing registered LSP servers and their status (ready/initializing/etc.), with document counts per language + +### Changed + +- **Shorter tool descriptions** — Condensed MCP tool descriptions for better compatibility with AI agent context windows + ## [0.3.0] - 2025-12-28 Major feature release adding LSP notification handling and 3 new MCP tools for real-time diagnostics and server monitoring. diff --git a/README.md b/README.md index 24652f2..e102b03 100644 --- a/README.md +++ b/README.md @@ -248,6 +248,7 @@ Claude: [get_references] Found 4 matches: | Tool | What it does | |------|--------------| +| `get_server_status` | Show registered LSP servers and their status | | `get_server_logs` | Debug LSP issues with internal log messages | | `get_server_messages` | User-facing messages from the language server | diff --git a/crates/mcpls-core/src/bridge/state.rs b/crates/mcpls-core/src/bridge/state.rs index d681861..44e2b1d 100644 --- a/crates/mcpls-core/src/bridge/state.rs +++ b/crates/mcpls-core/src/bridge/state.rs @@ -87,6 +87,12 @@ impl DocumentTracker { self.documents.is_empty() } + /// Get all tracked documents. + #[must_use] + pub const fn documents(&self) -> &HashMap { + &self.documents + } + /// Open a document and track its state. /// /// Returns the document URI for use in LSP requests. @@ -705,6 +711,39 @@ mod tests { } #[test] + fn test_documents_accessor_returns_empty_map_for_new_tracker() { + let tracker = DocumentTracker::new(); + let docs = tracker.documents(); + assert!(docs.is_empty()); + } + + #[test] + fn test_documents_accessor_returns_all_open_documents() { + let mut tracker = DocumentTracker::new(); + let path1 = PathBuf::from("/test/file1.rs"); + let path2 = PathBuf::from("/test/file2.rs"); + + tracker.open(path1.clone(), "content1".to_string()).unwrap(); + tracker.open(path2.clone(), "content2".to_string()).unwrap(); + + let docs = tracker.documents(); + assert_eq!(docs.len(), 2); + assert!(docs.contains_key(&path1)); + assert!(docs.contains_key(&path2)); + } + + #[test] + fn test_documents_accessor_reflects_document_state() { + let mut tracker = DocumentTracker::new(); + let path = PathBuf::from("/test/file.rs"); + + tracker.open(path.clone(), "initial".to_string()).unwrap(); + tracker.update(&path, "updated".to_string()); + + let docs = tracker.documents(); + let state = docs.get(&path).unwrap(); + assert_eq!(state.content, "updated"); + assert_eq!(state.version, 2); fn test_detect_language_with_custom_extension() { let mut map = HashMap::new(); map.insert("nu".to_string(), "nushell".to_string()); diff --git a/crates/mcpls-core/src/bridge/translator.rs b/crates/mcpls-core/src/bridge/translator.rs index 609f253..13ffd9c 100644 --- a/crates/mcpls-core/src/bridge/translator.rs +++ b/crates/mcpls-core/src/bridge/translator.rs @@ -417,6 +417,30 @@ pub struct ServerMessagesResult { pub messages: Vec, } +/// Status information for a single LSP server. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LspServerStatus { + /// Language identifier for the server. + pub language_id: String, + /// Current status of the server. + pub status: String, + /// Command used to start the server. + pub command: String, + /// Number of documents tracked by this server. + pub document_count: usize, +} + +/// Result of server status request. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ServerStatusResult { + /// List of server status entries. + pub servers: Vec, + /// Total number of servers. + pub total_servers: usize, + /// Total number of tracked documents across all servers. + pub document_count: usize, +} + /// Maximum allowed position value for validation. const MAX_POSITION_VALUE: u32 = 1_000_000; /// Maximum allowed range size in lines. @@ -1444,6 +1468,45 @@ impl Translator { let messages: Vec<_> = all_messages.iter().take(limit).cloned().collect(); Ok(ServerMessagesResult { messages }) } + + /// Handle server status request. + /// + /// Returns the status of all registered LSP servers, including their language ID, + /// current state, command, and number of tracked documents. + /// + /// # Errors + /// + /// This method does not return errors. + pub async fn handle_server_status(&self) -> Result { + let mut servers = Vec::new(); + + for (language_id, client) in &self.lsp_clients { + let state = client.state().await; + let config = client.config(); + + let document_count = self + .document_tracker + .documents() + .keys() + .filter(|path| detect_language(path) == *language_id) + .count(); + + servers.push(LspServerStatus { + language_id: language_id.clone(), + status: state.to_string(), + command: config.command.clone(), + document_count, + }); + } + + let total_servers = servers.len(); + let document_count = self.document_tracker.documents().len(); + Ok(ServerStatusResult { + servers, + total_servers, + document_count, + }) + } } /// Extract hover contents as markdown string. @@ -2691,6 +2754,289 @@ mod tests { } #[test] + fn test_lsp_server_status_creation() { + let status = LspServerStatus { + language_id: "rust".to_string(), + status: "ready".to_string(), + command: "rust-analyzer".to_string(), + document_count: 5, + }; + + assert_eq!(status.language_id, "rust"); + assert_eq!(status.status, "ready"); + assert_eq!(status.command, "rust-analyzer"); + assert_eq!(status.document_count, 5); + } + + #[test] + fn test_lsp_server_status_json_serialization() { + let status = LspServerStatus { + language_id: "rust".to_string(), + status: "initializing".to_string(), + command: "rust-analyzer".to_string(), + document_count: 0, + }; + + let json = serde_json::to_string(&status).unwrap(); + assert!(json.contains("\"language_id\":\"rust\"")); + assert!(json.contains("\"status\":\"initializing\"")); + assert!(json.contains("\"command\":\"rust-analyzer\"")); + assert!(json.contains("\"document_count\":0")); + } + + #[test] + fn test_lsp_server_status_json_deserialization() { + let json = r#"{"language_id":"python","status":"ready","command":"pylsp","document_count":3}"#; + let status: LspServerStatus = serde_json::from_str(json).unwrap(); + + assert_eq!(status.language_id, "python"); + assert_eq!(status.status, "ready"); + assert_eq!(status.command, "pylsp"); + assert_eq!(status.document_count, 3); + } + + #[test] + fn test_server_status_result_creation() { + let server1 = LspServerStatus { + language_id: "rust".to_string(), + status: "ready".to_string(), + command: "rust-analyzer".to_string(), + document_count: 5, + }; + let server2 = LspServerStatus { + language_id: "python".to_string(), + status: "initializing".to_string(), + command: "pylsp".to_string(), + document_count: 0, + }; + + let result = ServerStatusResult { + servers: vec![server1, server2], + total_servers: 2, + document_count: 0, + }; + + assert_eq!(result.servers.len(), 2); + assert_eq!(result.total_servers, 2); + assert_eq!(result.servers[0].language_id, "rust"); + assert_eq!(result.servers[1].language_id, "python"); + } + + #[test] + fn test_server_status_result_empty() { + let result = ServerStatusResult { + servers: vec![], + total_servers: 0, + document_count: 0, + }; + + let json = serde_json::to_string(&result).unwrap(); + assert!(json.contains("\"servers\":[]")); + assert!(json.contains("\"total_servers\":0")); + } + + #[test] + fn test_server_status_result_json_serialization() { + let server = LspServerStatus { + language_id: "rust".to_string(), + status: "ready".to_string(), + command: "rust-analyzer".to_string(), + document_count: 3, + }; + let result = ServerStatusResult { + servers: vec![server], + total_servers: 1, + document_count: 3, + }; + + let json = serde_json::to_string(&result).unwrap(); + assert!(json.contains("\"servers\":[")); + assert!(json.contains("\"total_servers\":1")); + assert!(json.contains("\"language_id\":\"rust\"")); + } + + #[test] + fn test_server_status_result_json_deserialization() { + let json = r#"{"servers":[{"language_id":"go","status":"uninitialized","command":"gopls","document_count":0}],"total_servers":1,"document_count":0}"#; + let result: ServerStatusResult = serde_json::from_str(json).unwrap(); + + assert_eq!(result.total_servers, 1); + assert_eq!(result.document_count, 0); + assert_eq!(result.servers.len(), 1); + assert_eq!(result.servers[0].language_id, "go"); + assert_eq!(result.servers[0].status, "uninitialized"); + } + + #[test] + fn test_lsp_server_status_all_valid_statuses() { + let valid_statuses = ["ready", "initializing", "uninitialized", "shutting_down", "shutdown"]; + + for status_value in valid_statuses { + let status = LspServerStatus { + language_id: "test".to_string(), + status: status_value.to_string(), + command: "test-cmd".to_string(), + document_count: 0, + }; + + let json = serde_json::to_string(&status).unwrap(); + assert!(json.contains(&format!("\"status\":\"{}\"", status_value))); + + let deserialized: LspServerStatus = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.status, status_value); + } + } + + #[tokio::test] + async fn test_handle_server_status_empty_workspace() { + let mut translator = Translator::new(); + + let result = translator.handle_server_status().await; + assert!(result.is_ok()); + + let status = result.unwrap(); + assert!(status.servers.is_empty()); + assert_eq!(status.total_servers, 0); + } + + #[tokio::test] + async fn test_handle_server_status_returns_server_status_result() { + let translator = Translator::new(); + + let result = translator.handle_server_status().await; + assert!(result.is_ok()); + + let status_result = result.unwrap(); + assert_eq!(status_result.servers.len(), status_result.total_servers); + } + + #[tokio::test] + async fn test_handle_server_status_with_registered_client() { + use crate::config::LspServerConfig; + + let mut translator = Translator::new(); + + let config = LspServerConfig::rust_analyzer(); + let client = LspClient::new(config); + translator.register_client("rust".to_string(), client); + + let result = translator.handle_server_status().await; + assert!(result.is_ok()); + + let status = result.unwrap(); + assert_eq!(status.total_servers, 1); + assert_eq!(status.servers.len(), 1); + + let server_status = &status.servers[0]; + assert_eq!(server_status.language_id, "rust"); + assert_eq!(server_status.command, "rust-analyzer"); + assert_eq!(server_status.status, "uninitialized"); + assert_eq!(server_status.document_count, 0); + } + + #[tokio::test] + async fn test_handle_server_status_multiple_servers() { + use crate::config::LspServerConfig; + + let mut translator = Translator::new(); + + let rust_config = LspServerConfig::rust_analyzer(); + let rust_client = LspClient::new(rust_config); + translator.register_client("rust".to_string(), rust_client); + + let python_config = LspServerConfig::pyright(); + let python_client = LspClient::new(python_config); + translator.register_client("python".to_string(), python_client); + + let result = translator.handle_server_status().await; + assert!(result.is_ok()); + + let status = result.unwrap(); + assert_eq!(status.total_servers, 2); + assert_eq!(status.servers.len(), 2); + + let language_ids: Vec<&str> = status.servers.iter().map(|s| s.language_id.as_str()).collect(); + assert!(language_ids.contains(&"rust")); + assert!(language_ids.contains(&"python")); + } + + #[tokio::test] + async fn test_handle_server_status_document_count() { + use crate::config::LspServerConfig; + + let mut translator = Translator::new(); + let temp_dir = TempDir::new().unwrap(); + + let test_file1 = temp_dir.path().join("test1.rs"); + let test_file2 = temp_dir.path().join("test2.rs"); + fs::write(&test_file1, "fn main() {}").unwrap(); + fs::write(&test_file2, "fn helper() {}").unwrap(); + + translator + .document_tracker_mut() + .open(test_file1, "fn main() {}".to_string()) + .unwrap(); + translator + .document_tracker_mut() + .open(test_file2, "fn helper() {}".to_string()) + .unwrap(); + + let config = LspServerConfig::rust_analyzer(); + let client = LspClient::new(config); + translator.register_client("rust".to_string(), client); + + let result = translator.handle_server_status().await; + assert!(result.is_ok()); + + let status = result.unwrap(); + assert_eq!(status.servers.len(), 1); + + let rust_server = &status.servers[0]; + assert_eq!(rust_server.language_id, "rust"); + assert_eq!(rust_server.document_count, 2); + } + + #[tokio::test] + async fn test_handle_server_status_document_count_per_language() { + use crate::config::LspServerConfig; + + let mut translator = Translator::new(); + let temp_dir = TempDir::new().unwrap(); + + let rust_file = temp_dir.path().join("test.rs"); + let python_file = temp_dir.path().join("test.py"); + fs::write(&rust_file, "fn main() {}").unwrap(); + fs::write(&python_file, "def main(): pass").unwrap(); + + translator + .document_tracker_mut() + .open(rust_file, "fn main() {}".to_string()) + .unwrap(); + translator + .document_tracker_mut() + .open(python_file, "def main(): pass".to_string()) + .unwrap(); + + let rust_config = LspServerConfig::rust_analyzer(); + let rust_client = LspClient::new(rust_config); + translator.register_client("rust".to_string(), rust_client); + + let python_config = LspServerConfig::pyright(); + let python_client = LspClient::new(python_config); + translator.register_client("python".to_string(), python_client); + + let result = translator.handle_server_status().await; + assert!(result.is_ok()); + + let status = result.unwrap(); + assert_eq!(status.total_servers, 2); + + for server in &status.servers { + if server.language_id == "rust" { + assert_eq!(server.document_count, 1); + } else if server.language_id == "python" { + assert_eq!(server.document_count, 1); + } fn test_translator_with_custom_extensions() { let mut extension_map = HashMap::new(); extension_map.insert("nu".to_string(), "nushell".to_string()); @@ -2752,6 +3098,43 @@ mod tests { } #[tokio::test] + async fn test_handle_server_status_status_lowercase() { + use crate::config::LspServerConfig; + + let mut translator = Translator::new(); + + let config = LspServerConfig::rust_analyzer(); + let client = LspClient::new(config); + translator.register_client("rust".to_string(), client); + + let result = translator.handle_server_status().await; + assert!(result.is_ok()); + + let status = result.unwrap(); + let server_status = &status.servers[0]; + + let valid_statuses = ["ready", "initializing", "uninitialized", "shutting_down", "shutdown"]; + assert!( + valid_statuses.contains(&server_status.status.as_str()), + "Status '{}' should be lowercase", + server_status.status + ); + } + + #[tokio::test] + async fn test_handle_server_status_json_serializable() { + let mut translator = Translator::new(); + + let result = translator.handle_server_status().await; + assert!(result.is_ok()); + + let status = result.unwrap(); + let json_result = serde_json::to_string(&status); + assert!(json_result.is_ok()); + + let json = json_result.unwrap(); + assert!(json.contains("\"servers\"")); + assert!(json.contains("\"total_servers\"")); async fn test_serve_initializes_translator_with_extensions() { use crate::config::{LanguageExtensionMapping, WorkspaceConfig}; diff --git a/crates/mcpls-core/src/lsp/client.rs b/crates/mcpls-core/src/lsp/client.rs index feba0e2..51493d4 100644 --- a/crates/mcpls-core/src/lsp/client.rs +++ b/crates/mcpls-core/src/lsp/client.rs @@ -170,6 +170,12 @@ impl LspClient { *self.state.lock().await } + /// Get the configuration for this client. + #[must_use] + pub const fn config(&self) -> &LspServerConfig { + &self.config + } + /// Send request and wait for response with timeout. /// /// # Type Parameters @@ -631,4 +637,64 @@ mod tests { fn test_jsonrpc_version_constant() { assert_eq!(JSONRPC_VERSION, "2.0"); } + + #[tokio::test] + async fn test_state_returns_current_state() { + let config = LspServerConfig::rust_analyzer(); + let client = LspClient::new(config); + + let state = client.state().await; + assert_eq!(state, super::super::ServerState::Uninitialized); + } + + #[test] + fn test_config_returns_config_reference() { + let config = LspServerConfig::rust_analyzer(); + let client = LspClient::new(config); + + let config_ref = client.config(); + assert_eq!(config_ref.language_id, "rust"); + assert_eq!(config_ref.command, "rust-analyzer"); + } + + #[test] + fn test_config_returns_custom_config() { + use std::collections::HashMap; + + let mut env = HashMap::new(); + env.insert("TEST_VAR".to_string(), "test_value".to_string()); + + let config = LspServerConfig { + language_id: "custom".to_string(), + command: "custom-server".to_string(), + args: vec!["--arg1".to_string()], + env, + file_patterns: vec!["**/*.custom".to_string()], + initialization_options: Some(serde_json::json!({"option": true})), + timeout_seconds: 45, + }; + let client = LspClient::new(config); + + let config_ref = client.config(); + assert_eq!(config_ref.language_id, "custom"); + assert_eq!(config_ref.command, "custom-server"); + assert_eq!(config_ref.args, vec!["--arg1"]); + assert_eq!(config_ref.env.get("TEST_VAR"), Some(&"test_value".to_string())); + assert_eq!(config_ref.timeout_seconds, 45); + } + + #[test] + fn test_config_same_after_clone() { + let config = LspServerConfig::pyright(); + let client = LspClient::new(config); + let cloned = client.clone(); + + let orig_config = client.config(); + let cloned_config = cloned.config(); + + assert_eq!(orig_config.language_id, cloned_config.language_id); + assert_eq!(orig_config.command, cloned_config.command); + assert_eq!(orig_config.args, cloned_config.args); + assert_eq!(orig_config.timeout_seconds, cloned_config.timeout_seconds); + } } diff --git a/crates/mcpls-core/src/lsp/lifecycle.rs b/crates/mcpls-core/src/lsp/lifecycle.rs index fcef85b..8c2064c 100644 --- a/crates/mcpls-core/src/lsp/lifecycle.rs +++ b/crates/mcpls-core/src/lsp/lifecycle.rs @@ -54,6 +54,19 @@ impl ServerState { } } +impl std::fmt::Display for ServerState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + Self::Uninitialized => "uninitialized", + Self::Initializing => "initializing", + Self::Ready => "ready", + Self::ShuttingDown => "shutting_down", + Self::Shutdown => "shutdown", + }; + write!(f, "{s}") + } +} + /// Configuration for LSP server initialization. #[derive(Debug, Clone)] pub struct ServerInitConfig { @@ -527,6 +540,31 @@ mod tests { assert!(debug_str.contains("Ready")); } + #[test] + fn test_server_state_display_ready() { + assert_eq!(format!("{}", ServerState::Ready), "ready"); + } + + #[test] + fn test_server_state_display_initializing() { + assert_eq!(format!("{}", ServerState::Initializing), "initializing"); + } + + #[test] + fn test_server_state_display_uninitialized() { + assert_eq!(format!("{}", ServerState::Uninitialized), "uninitialized"); + } + + #[test] + fn test_server_state_display_shutting_down() { + assert_eq!(format!("{}", ServerState::ShuttingDown), "shutting_down"); + } + + #[test] + fn test_server_state_display_shutdown() { + assert_eq!(format!("{}", ServerState::Shutdown), "shutdown"); + } + #[test] fn test_server_init_config_clone() { let config = ServerInitConfig { diff --git a/crates/mcpls-core/src/mcp/server.rs b/crates/mcpls-core/src/mcp/server.rs index 4519f87..d29fd33 100644 --- a/crates/mcpls-core/src/mcp/server.rs +++ b/crates/mcpls-core/src/mcp/server.rs @@ -15,7 +15,7 @@ use super::tools::{ CachedDiagnosticsParams, CallHierarchyCallsParams, CallHierarchyPrepareParams, CodeActionsParams, CompletionsParams, DefinitionParams, DiagnosticsParams, DocumentSymbolsParams, FormatDocumentParams, HoverParams, ReferencesParams, RenameParams, - ServerLogsParams, ServerMessagesParams, WorkspaceSymbolParams, + ServerLogsParams, ServerMessagesParams, ServerStatusParams, WorkspaceSymbolParams, }; use crate::bridge::Translator; @@ -422,6 +422,26 @@ impl McplsServer { Err(e) => Err(McpError::internal_error(e.to_string(), None)), } } + + /// Get the status of all registered LSP servers. + #[tool( + description = "Status of all registered LSP servers. Returns server state, command, and document counts." + )] + async fn get_server_status( + &self, + Parameters(ServerStatusParams {}): Parameters, + ) -> Result { + let result = { + let translator = self.context.translator.lock().await; + translator.handle_server_status().await + }; + + match result { + Ok(value) => serde_json::to_string(&value) + .map_err(|e| McpError::internal_error(format!("Serialization error: {e}"), None)), + Err(e) => Err(McpError::internal_error(e.to_string(), None)), + } + } } #[tool_handler] @@ -838,4 +858,49 @@ mod tests { let result = server.get_server_messages(params).await; assert!(result.is_ok()); } + + #[tokio::test] + async fn test_server_status_tool_empty_workspace() { + let server = create_test_server(); + let params = Parameters(ServerStatusParams {}); + + let result = server.get_server_status(params).await; + assert!(result.is_ok()); + + let json_str = result.unwrap(); + let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap(); + assert!(parsed.get("servers").is_some()); + + let servers = parsed.get("servers").unwrap().as_array().unwrap(); + assert_eq!(servers.len(), 0); + } + + #[tokio::test] + async fn test_server_status_tool_returns_json() { + let server = create_test_server(); + let params = Parameters(ServerStatusParams {}); + + let result = server.get_server_status(params).await; + assert!(result.is_ok()); + + let json_str = result.unwrap(); + let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap(); + assert!(parsed.is_object()); + } + + #[tokio::test] + async fn test_server_status_tool_document_count() { + let server = create_test_server(); + let params = Parameters(ServerStatusParams {}); + + let result = server.get_server_status(params).await; + assert!(result.is_ok()); + + let json_str = result.unwrap(); + let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap(); + assert!(parsed.get("document_count").is_some()); + + let count = parsed.get("document_count").unwrap().as_u64().unwrap(); + assert_eq!(count, 0); + } } diff --git a/crates/mcpls-core/src/mcp/tools.rs b/crates/mcpls-core/src/mcp/tools.rs index f7c216b..bd7ba49 100644 --- a/crates/mcpls-core/src/mcp/tools.rs +++ b/crates/mcpls-core/src/mcp/tools.rs @@ -249,3 +249,70 @@ pub struct ServerMessagesParams { const fn default_message_limit() -> usize { 20 } + +/// Parameters for the `get_server_status` tool. +#[derive(Debug, Clone, Default, Serialize, JsonSchema)] +#[schemars(description = "Parameters for getting the current status of all LSP servers.")] +pub struct ServerStatusParams {} + +impl<'de> Deserialize<'de> for ServerStatusParams { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + // Accept both null and {} as valid empty params + let value = Option::::deserialize(deserializer)?; + match value { + None | Some(serde_json::Value::Object(_) | serde_json::Value::Null) => { + Ok(Self {}) + } + Some(_) => Err(serde::de::Error::custom( + "expected null or object for ServerStatusParams", + )), + } + } +} + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +mod tests { + use super::*; + + #[test] + fn server_status_params_can_be_created() { + let _params = ServerStatusParams {}; + } + + #[test] + fn server_status_params_serializes_to_empty_object() { + let params = ServerStatusParams {}; + let json = serde_json::to_string(¶ms).unwrap(); + assert_eq!(json, "{}"); + } + + #[test] + fn server_status_params_deserializes_from_empty_object() { + let params: ServerStatusParams = serde_json::from_str("{}").unwrap(); + let _ = params; + } + + #[test] + fn server_status_params_deserializes_from_null() { + let params: ServerStatusParams = serde_json::from_str("null").unwrap(); + let _ = params; + } + + #[test] + fn server_status_params_implements_debug() { + let params = ServerStatusParams {}; + let debug_str = format!("{:?}", params); + assert!(debug_str.contains("ServerStatusParams")); + } + + #[test] + fn server_status_params_implements_clone() { + let params = ServerStatusParams {}; + let cloned = params.clone(); + let _ = cloned; + } +} diff --git a/docs/user-guide/tools-reference.md b/docs/user-guide/tools-reference.md index 23828cf..753b6b0 100644 --- a/docs/user-guide/tools-reference.md +++ b/docs/user-guide/tools-reference.md @@ -1,6 +1,6 @@ # MCP Tools Reference -Complete reference for all 16 MCP tools provided by mcpls. +Complete reference for all 17 MCP tools provided by mcpls. ## Overview @@ -46,6 +46,7 @@ mcpls exposes semantic code intelligence from Language Server Protocol (LSP) ser | Tool | Description | |------|-------------| +| [get_server_status](#get_server_status) | Get registered LSP servers and their status | | [get_server_logs](#get_server_logs) | Get LSP server log messages | | [get_server_messages](#get_server_messages) | Get LSP server show messages | @@ -813,6 +814,76 @@ Get diagnostics from LSP server push notifications (cached). --- +## get_server_status + +Get the status of all registered LSP servers. + +### Parameters + +```json +{} +``` + +No parameters required. + +### Returns + +```json +{ + "servers": [ + { + "language_id": "rust", + "status": "ready", + "command": "rust-analyzer", + "document_count": 5 + }, + { + "language_id": "python", + "status": "initializing", + "command": "pyright-langserver", + "document_count": 0 + } + ], + "total_servers": 2, + "document_count": 5 +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `servers` | array | List of registered LSP servers | +| `servers[].language_id` | string | Language identifier (rust, python, etc.) | +| `servers[].status` | string | Server state: ready, initializing, uninitialized, shutting_down, shutdown | +| `servers[].command` | string | Server command (e.g., rust-analyzer) | +| `servers[].document_count` | integer | Number of open documents for this language | +| `total_servers` | integer | Total number of registered servers | +| `document_count` | integer | Total documents across all servers | + +### Example Use Cases + +**Check server health:** +``` +User: Which language servers are running? +Claude: [Uses get_server_status] 2 servers registered: + - rust: ready (rust-analyzer, 5 documents) + - python: initializing (pyright-langserver) +``` + +**Debug initialization:** +``` +User: Why isn't completion working for Python files? +Claude: [Uses get_server_status] The Python server is still initializing. + Wait a moment for it to become ready. +``` + +### Notes + +- Returns empty `servers` array if no LSP servers are configured +- Status values are always lowercase strings +- Document count reflects currently open/tracked documents + +--- + ## get_server_logs Get recent log messages from LSP servers.