Skip to content

Commit a42f9fa

Browse files
srnnklsclaude
andcommitted
feat(status): implement handle_server_status handler
Tasks: STATUS-006 Batch: 2/3 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 68d6679 commit a42f9fa

4 files changed

Lines changed: 274 additions & 10 deletions

File tree

crates/mcpls-core/src/bridge/translator.rs

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1451,6 +1451,43 @@ impl Translator {
14511451
let messages: Vec<_> = all_messages.iter().take(limit).cloned().collect();
14521452
Ok(ServerMessagesResult { messages })
14531453
}
1454+
1455+
/// Handle server status request.
1456+
///
1457+
/// Returns the status of all registered LSP servers, including their language ID,
1458+
/// current state, command, and number of tracked documents.
1459+
///
1460+
/// # Errors
1461+
///
1462+
/// This method does not return errors.
1463+
pub async fn handle_server_status(&self) -> Result<ServerStatusResult> {
1464+
let mut servers = Vec::new();
1465+
1466+
for (language_id, client) in &self.lsp_clients {
1467+
let state = client.state().await;
1468+
let config = client.config();
1469+
1470+
let document_count = self
1471+
.document_tracker
1472+
.documents()
1473+
.keys()
1474+
.filter(|path| detect_language(path) == *language_id)
1475+
.count();
1476+
1477+
servers.push(LspServerStatus {
1478+
language_id: language_id.clone(),
1479+
status: state.to_string(),
1480+
command: config.command.clone(),
1481+
document_count,
1482+
});
1483+
}
1484+
1485+
let total_servers = servers.len();
1486+
Ok(ServerStatusResult {
1487+
servers,
1488+
total_servers,
1489+
})
1490+
}
14541491
}
14551492

14561493
/// Extract hover contents as markdown string.
@@ -2826,4 +2863,197 @@ mod tests {
28262863
assert_eq!(deserialized.status, status_value);
28272864
}
28282865
}
2866+
2867+
#[tokio::test]
2868+
async fn test_handle_server_status_empty_workspace() {
2869+
let mut translator = Translator::new();
2870+
2871+
let result = translator.handle_server_status().await;
2872+
assert!(result.is_ok());
2873+
2874+
let status = result.unwrap();
2875+
assert!(status.servers.is_empty());
2876+
assert_eq!(status.total_servers, 0);
2877+
}
2878+
2879+
#[tokio::test]
2880+
async fn test_handle_server_status_returns_server_status_result() {
2881+
let translator = Translator::new();
2882+
2883+
let result = translator.handle_server_status().await;
2884+
assert!(result.is_ok());
2885+
2886+
let status_result = result.unwrap();
2887+
assert_eq!(status_result.servers.len(), status_result.total_servers);
2888+
}
2889+
2890+
#[tokio::test]
2891+
async fn test_handle_server_status_with_registered_client() {
2892+
use crate::config::LspServerConfig;
2893+
2894+
let mut translator = Translator::new();
2895+
2896+
let config = LspServerConfig::rust_analyzer();
2897+
let client = LspClient::new(config);
2898+
translator.register_client("rust".to_string(), client);
2899+
2900+
let result = translator.handle_server_status().await;
2901+
assert!(result.is_ok());
2902+
2903+
let status = result.unwrap();
2904+
assert_eq!(status.total_servers, 1);
2905+
assert_eq!(status.servers.len(), 1);
2906+
2907+
let server_status = &status.servers[0];
2908+
assert_eq!(server_status.language_id, "rust");
2909+
assert_eq!(server_status.command, "rust-analyzer");
2910+
assert_eq!(server_status.status, "uninitialized");
2911+
assert_eq!(server_status.document_count, 0);
2912+
}
2913+
2914+
#[tokio::test]
2915+
async fn test_handle_server_status_multiple_servers() {
2916+
use crate::config::LspServerConfig;
2917+
2918+
let mut translator = Translator::new();
2919+
2920+
let rust_config = LspServerConfig::rust_analyzer();
2921+
let rust_client = LspClient::new(rust_config);
2922+
translator.register_client("rust".to_string(), rust_client);
2923+
2924+
let python_config = LspServerConfig::pyright();
2925+
let python_client = LspClient::new(python_config);
2926+
translator.register_client("python".to_string(), python_client);
2927+
2928+
let result = translator.handle_server_status().await;
2929+
assert!(result.is_ok());
2930+
2931+
let status = result.unwrap();
2932+
assert_eq!(status.total_servers, 2);
2933+
assert_eq!(status.servers.len(), 2);
2934+
2935+
let language_ids: Vec<&str> = status.servers.iter().map(|s| s.language_id.as_str()).collect();
2936+
assert!(language_ids.contains(&"rust"));
2937+
assert!(language_ids.contains(&"python"));
2938+
}
2939+
2940+
#[tokio::test]
2941+
async fn test_handle_server_status_document_count() {
2942+
use crate::config::LspServerConfig;
2943+
2944+
let mut translator = Translator::new();
2945+
let temp_dir = TempDir::new().unwrap();
2946+
2947+
let test_file1 = temp_dir.path().join("test1.rs");
2948+
let test_file2 = temp_dir.path().join("test2.rs");
2949+
fs::write(&test_file1, "fn main() {}").unwrap();
2950+
fs::write(&test_file2, "fn helper() {}").unwrap();
2951+
2952+
translator
2953+
.document_tracker_mut()
2954+
.open(test_file1, "fn main() {}".to_string())
2955+
.unwrap();
2956+
translator
2957+
.document_tracker_mut()
2958+
.open(test_file2, "fn helper() {}".to_string())
2959+
.unwrap();
2960+
2961+
let config = LspServerConfig::rust_analyzer();
2962+
let client = LspClient::new(config);
2963+
translator.register_client("rust".to_string(), client);
2964+
2965+
let result = translator.handle_server_status().await;
2966+
assert!(result.is_ok());
2967+
2968+
let status = result.unwrap();
2969+
assert_eq!(status.servers.len(), 1);
2970+
2971+
let rust_server = &status.servers[0];
2972+
assert_eq!(rust_server.language_id, "rust");
2973+
assert_eq!(rust_server.document_count, 2);
2974+
}
2975+
2976+
#[tokio::test]
2977+
async fn test_handle_server_status_document_count_per_language() {
2978+
use crate::config::LspServerConfig;
2979+
2980+
let mut translator = Translator::new();
2981+
let temp_dir = TempDir::new().unwrap();
2982+
2983+
let rust_file = temp_dir.path().join("test.rs");
2984+
let python_file = temp_dir.path().join("test.py");
2985+
fs::write(&rust_file, "fn main() {}").unwrap();
2986+
fs::write(&python_file, "def main(): pass").unwrap();
2987+
2988+
translator
2989+
.document_tracker_mut()
2990+
.open(rust_file, "fn main() {}".to_string())
2991+
.unwrap();
2992+
translator
2993+
.document_tracker_mut()
2994+
.open(python_file, "def main(): pass".to_string())
2995+
.unwrap();
2996+
2997+
let rust_config = LspServerConfig::rust_analyzer();
2998+
let rust_client = LspClient::new(rust_config);
2999+
translator.register_client("rust".to_string(), rust_client);
3000+
3001+
let python_config = LspServerConfig::pyright();
3002+
let python_client = LspClient::new(python_config);
3003+
translator.register_client("python".to_string(), python_client);
3004+
3005+
let result = translator.handle_server_status().await;
3006+
assert!(result.is_ok());
3007+
3008+
let status = result.unwrap();
3009+
assert_eq!(status.total_servers, 2);
3010+
3011+
for server in &status.servers {
3012+
if server.language_id == "rust" {
3013+
assert_eq!(server.document_count, 1);
3014+
} else if server.language_id == "python" {
3015+
assert_eq!(server.document_count, 1);
3016+
}
3017+
}
3018+
}
3019+
3020+
#[tokio::test]
3021+
async fn test_handle_server_status_status_lowercase() {
3022+
use crate::config::LspServerConfig;
3023+
3024+
let mut translator = Translator::new();
3025+
3026+
let config = LspServerConfig::rust_analyzer();
3027+
let client = LspClient::new(config);
3028+
translator.register_client("rust".to_string(), client);
3029+
3030+
let result = translator.handle_server_status().await;
3031+
assert!(result.is_ok());
3032+
3033+
let status = result.unwrap();
3034+
let server_status = &status.servers[0];
3035+
3036+
let valid_statuses = ["ready", "initializing", "uninitialized", "shutting_down", "shutdown"];
3037+
assert!(
3038+
valid_statuses.contains(&server_status.status.as_str()),
3039+
"Status '{}' should be lowercase",
3040+
server_status.status
3041+
);
3042+
}
3043+
3044+
#[tokio::test]
3045+
async fn test_handle_server_status_json_serializable() {
3046+
let mut translator = Translator::new();
3047+
3048+
let result = translator.handle_server_status().await;
3049+
assert!(result.is_ok());
3050+
3051+
let status = result.unwrap();
3052+
let json_result = serde_json::to_string(&status);
3053+
assert!(json_result.is_ok());
3054+
3055+
let json = json_result.unwrap();
3056+
assert!(json.contains("\"servers\""));
3057+
assert!(json.contains("\"total_servers\""));
3058+
}
28293059
}

specs/active/add-status-tool/checkpoint.yaml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@ checkpoint:
33
spec_path: ./specs/active/add-status-tool
44
branch: feat/add-status-tool
55
timestamp: 2026-01-23T00:00:00Z
6-
last_batch: 1
6+
last_batch: 2
77
last_commit: pending
88
tasks:
9-
completed: [STATUS-001, STATUS-002, STATUS-003, STATUS-004, STATUS-005]
10-
pending: [STATUS-006, STATUS-007]
9+
completed: [STATUS-001, STATUS-002, STATUS-003, STATUS-004, STATUS-005, STATUS-006]
10+
pending: [STATUS-007]
1111
next_batch:
12-
number: 2
13-
tasks: [STATUS-006]
12+
number: 3
13+
tasks: [STATUS-007]
1414
deferred_issues:
15-
- "Dead code warnings for foundation types (will resolve in Batch 2/3)"
15+
- "Dead code warning for ServerStatusParams (will resolve in Batch 3)"
16+
- "Unnecessary mut in 2 test functions (minor)"
1617
review_config:
1718
reviewers: [claude-opus]

specs/active/add-status-tool/review.yaml

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,33 @@ batch_reviews:
2626
High-severity style issues (missing const, use_self) were fixed.
2727
Medium-severity dead_code warnings are expected for foundation types.
2828
29+
- batch: 2
30+
timestamp: 2026-01-23T00:00:00Z
31+
tasks: [STATUS-006]
32+
reviewers:
33+
- id: claude-opus
34+
status: completed
35+
gates:
36+
correctness: pass
37+
style: pass
38+
performance: pass
39+
security: pass
40+
architecture: pass
41+
synthesized:
42+
gates:
43+
correctness: pass
44+
style: pass
45+
performance: pass
46+
security: pass
47+
architecture: pass
48+
critical_issues: 0
49+
high_issues: 0
50+
medium_issues: 1
51+
outcome: approved
52+
notes: |
53+
Async handler implementation correct. Tests comprehensive (8/8 passed).
54+
Medium issue: unnecessary mut in 2 test functions (optional fix).
55+
2956
issues:
3057
critical: []
3158
high: []
@@ -36,10 +63,16 @@ issues:
3663
line: 256
3764
resolution: deferred
3865
- task: STATUS-002
39-
description: "Dead code warnings for LspServerStatus and ServerStatusResult (expected, will resolve in STATUS-006/007)"
66+
description: "Dead code warnings for LspServerStatus and ServerStatusResult (partially resolved in STATUS-006)"
4067
file: /Users/srnnkls/projects/mcpls/crates/mcpls-core/src/bridge/translator.rs
4168
lines: [407, 420]
4269
resolution: deferred
70+
- task: STATUS-006
71+
description: "Unnecessary mut in test functions (lines 2869, 3046)"
72+
file: /Users/srnnkls/projects/mcpls/crates/mcpls-core/src/bridge/translator.rs
73+
lines: [2869, 3046]
74+
resolution: deferred
4375

4476
deferred_issues:
45-
- "Dead code warnings for foundation types - will resolve when integrated in Batch 2 and 3"
77+
- "Dead code warning for ServerStatusParams - will resolve in STATUS-007"
78+
- "Unnecessary mut in 2 test functions - compiler warning only, doesn't affect correctness"

specs/active/add-status-tool/tasks.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ tasks:
2424
active_form: Adding documents accessor to DocumentTracker
2525
- id: STATUS-006
2626
content: Implement handle_server_status() async handler in Translator
27-
status: pending
27+
status: completed
2828
active_form: Implementing async handler in Translator
2929
- id: STATUS-007
3030
content: Add get_server_status MCP tool definition in server.rs
@@ -33,4 +33,4 @@ tasks:
3333
meta:
3434
created: 2026-01-23
3535
last_updated: 2026-01-23
36-
progress: 5/7
36+
progress: 6/7

0 commit comments

Comments
 (0)