use std::sync::Arc;
use rmcp::handler::server::wrapper::Parameters;
use rmcp::model::{
Implementation, ListResourcesResult, RawResource, ReadResourceRequestParams,
ReadResourceResult, ResourceContents, ServerCapabilities, ServerInfo, SubscribeRequestParams,
UnsubscribeRequestParams,
};
use rmcp::{ErrorData as McpError, RoleServer, ServerHandler, tool, tool_handler, tool_router};
use tokio::sync::Mutex;
use super::handlers::HandlerContext;
use super::tools::{
CachedDiagnosticsParams, CallHierarchyCallsParams, CallHierarchyPrepareParams,
CodeActionsParams, CompletionsParams, DefinitionParams, DiagnosticsParams,
DocumentSymbolsParams, FormatDocumentParams, GoToImplementationParams,
GoToTypeDefinitionParams, HoverParams, InlayHintsParams, ReferencesParams, RenameParams,
ServerLogsParams, ServerMessagesParams, SignatureHelpParams, WorkspaceSymbolParams,
};
use crate::bridge::resources::{make_uri, parse_uri};
use crate::bridge::{ResourceSubscriptions, Translator};
#[derive(Clone)]
pub struct McplsServer {
context: Arc<HandlerContext>,
}
#[tool_router]
impl McplsServer {
#[must_use]
pub fn new(
translator: Arc<Mutex<Translator>>,
subscriptions: Arc<ResourceSubscriptions>,
) -> Self {
let context = Arc::new(HandlerContext::new(translator, subscriptions));
Self { context }
}
#[tool(
description = "Type and documentation info at position. Returns signatures, docs, and inferred types for symbols."
)]
async fn get_hover(
&self,
Parameters(HoverParams {
file_path,
line,
character,
}): Parameters<HoverParams>,
) -> Result<String, McpError> {
let result = {
let mut translator = self.context.translator.lock().await;
translator.handle_hover(file_path, line, character).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(
description = "Definition location of symbol at position. Returns file path, line, and character where declared."
)]
async fn get_definition(
&self,
Parameters(DefinitionParams {
file_path,
line,
character,
}): Parameters<DefinitionParams>,
) -> Result<String, McpError> {
let result = {
let mut translator = self.context.translator.lock().await;
translator
.handle_definition(file_path, line, character)
.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(
description = "All references to symbol at position. Returns locations across workspace where symbol is used."
)]
async fn get_references(
&self,
Parameters(ReferencesParams {
file_path,
line,
character,
include_declaration,
}): Parameters<ReferencesParams>,
) -> Result<String, McpError> {
let result = {
let mut translator = self.context.translator.lock().await;
translator
.handle_references(file_path, line, character, include_declaration)
.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(
description = "Diagnostics for a file. Returns errors, warnings, and hints with severity and location."
)]
async fn get_diagnostics(
&self,
Parameters(DiagnosticsParams { file_path }): Parameters<DiagnosticsParams>,
) -> Result<String, McpError> {
let result = {
let mut translator = self.context.translator.lock().await;
translator.handle_diagnostics(file_path).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(
description = "Rename symbol across workspace. Returns text edits for all files where symbol is used."
)]
async fn rename_symbol(
&self,
Parameters(RenameParams {
file_path,
line,
character,
new_name,
}): Parameters<RenameParams>,
) -> Result<String, McpError> {
let result = {
let mut translator = self.context.translator.lock().await;
translator
.handle_rename(file_path, line, character, new_name)
.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(
description = "Completion suggestions at position. Returns methods, functions, variables, types, and snippets."
)]
async fn get_completions(
&self,
Parameters(CompletionsParams {
file_path,
line,
character,
trigger,
}): Parameters<CompletionsParams>,
) -> Result<String, McpError> {
let result = {
let mut translator = self.context.translator.lock().await;
translator
.handle_completions(file_path, line, character, trigger)
.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(
description = "Symbols in a file. Returns hierarchical outline with functions, classes, structs, and locations."
)]
async fn get_document_symbols(
&self,
Parameters(DocumentSymbolsParams { file_path }): Parameters<DocumentSymbolsParams>,
) -> Result<String, McpError> {
let result = {
let mut translator = self.context.translator.lock().await;
translator.handle_document_symbols(file_path).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(
description = "Format document with language-specific rules. Returns text edits for indentation, spacing, and style."
)]
async fn format_document(
&self,
Parameters(FormatDocumentParams {
file_path,
tab_size,
insert_spaces,
}): Parameters<FormatDocumentParams>,
) -> Result<String, McpError> {
let result = {
let mut translator = self.context.translator.lock().await;
translator
.handle_format_document(file_path, tab_size, insert_spaces)
.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(
description = "Search workspace symbols by name. Supports partial matching and fuzzy search."
)]
async fn workspace_symbol_search(
&self,
Parameters(WorkspaceSymbolParams {
query,
kind_filter,
limit,
}): Parameters<WorkspaceSymbolParams>,
) -> Result<String, McpError> {
let result = {
let mut translator = self.context.translator.lock().await;
translator
.handle_workspace_symbol(query, kind_filter, limit)
.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(
description = "Code actions for range. Returns quick fixes, refactorings, and source actions with edits."
)]
async fn get_code_actions(
&self,
Parameters(CodeActionsParams {
file_path,
start_line,
start_character,
end_line,
end_character,
kind_filter,
}): Parameters<CodeActionsParams>,
) -> Result<String, McpError> {
let result = {
let mut translator = self.context.translator.lock().await;
translator
.handle_code_actions(
file_path,
start_line,
start_character,
end_line,
end_character,
kind_filter,
)
.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(
description = "Prepare call hierarchy at position. Returns callable items for incoming/outgoing call analysis."
)]
async fn prepare_call_hierarchy(
&self,
Parameters(CallHierarchyPrepareParams {
file_path,
line,
character,
}): Parameters<CallHierarchyPrepareParams>,
) -> Result<String, McpError> {
let result = {
let mut translator = self.context.translator.lock().await;
translator
.handle_call_hierarchy_prepare(file_path, line, character)
.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(
description = "Functions calling the specified item. Takes call hierarchy item, returns all callers."
)]
async fn get_incoming_calls(
&self,
Parameters(CallHierarchyCallsParams { item }): Parameters<CallHierarchyCallsParams>,
) -> Result<String, McpError> {
let result = {
let mut translator = self.context.translator.lock().await;
translator.handle_incoming_calls(item).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(
description = "Functions called by the specified item. Takes call hierarchy item, returns all callees."
)]
async fn get_outgoing_calls(
&self,
Parameters(CallHierarchyCallsParams { item }): Parameters<CallHierarchyCallsParams>,
) -> Result<String, McpError> {
let result = {
let mut translator = self.context.translator.lock().await;
translator.handle_outgoing_calls(item).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(
description = "Cached diagnostics from server notifications. Faster than get_diagnostics, no new analysis."
)]
async fn get_cached_diagnostics(
&self,
Parameters(CachedDiagnosticsParams { file_path }): Parameters<CachedDiagnosticsParams>,
) -> Result<String, McpError> {
let result = {
let mut translator = self.context.translator.lock().await;
translator.handle_cached_diagnostics(&file_path)
};
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(
description = "Recent server log messages. Filter by level (error, warning, info, debug) for debugging."
)]
async fn get_server_logs(
&self,
Parameters(ServerLogsParams { limit, min_level }): Parameters<ServerLogsParams>,
) -> Result<String, McpError> {
let result = {
let mut translator = self.context.translator.lock().await;
translator.handle_server_logs(limit, min_level)
};
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(
description = "Recent server messages (showMessage notifications). User-facing prompts and status updates."
)]
async fn get_server_messages(
&self,
Parameters(ServerMessagesParams { limit }): Parameters<ServerMessagesParams>,
) -> Result<String, McpError> {
let result = {
let mut translator = self.context.translator.lock().await;
translator.handle_server_messages(limit)
};
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(
description = "Signature help at position. Returns parameter info, active signature/parameter, and documentation while typing a call."
)]
async fn get_signature_help(
&self,
Parameters(SignatureHelpParams {
file_path,
line,
character,
}): Parameters<SignatureHelpParams>,
) -> Result<String, McpError> {
let result = {
let mut translator = self.context.translator.lock().await;
translator
.handle_signature_help(file_path, line, character)
.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(
description = "Implementation locations of trait method or interface member at position."
)]
async fn go_to_implementation(
&self,
Parameters(GoToImplementationParams {
file_path,
line,
character,
}): Parameters<GoToImplementationParams>,
) -> Result<String, McpError> {
let result = {
let mut translator = self.context.translator.lock().await;
translator
.handle_implementation(file_path, line, character)
.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(
description = "Type definition location of expression at position. Distinct from go-to-definition for variable bindings."
)]
async fn go_to_type_definition(
&self,
Parameters(GoToTypeDefinitionParams {
file_path,
line,
character,
}): Parameters<GoToTypeDefinitionParams>,
) -> Result<String, McpError> {
let result = {
let mut translator = self.context.translator.lock().await;
translator
.handle_type_definition(file_path, line, character)
.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(
description = "Inlay hints in range. Returns inferred type/parameter annotations the editor would render inline."
)]
async fn get_inlay_hints(
&self,
Parameters(InlayHintsParams {
file_path,
start_line,
start_character,
end_line,
end_character,
}): Parameters<InlayHintsParams>,
) -> Result<String, McpError> {
let result = {
let mut translator = self.context.translator.lock().await;
translator
.handle_inlay_hints(
file_path,
start_line,
start_character,
end_line,
end_character,
)
.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]
impl ServerHandler for McplsServer {
async fn list_resources(
&self,
_request: Option<rmcp::model::PaginatedRequestParams>,
_context: rmcp::service::RequestContext<RoleServer>,
) -> Result<ListResourcesResult, McpError> {
let resources: Vec<_> = {
let translator = self.context.translator.lock().await;
translator
.document_tracker()
.open_paths()
.filter_map(|path| {
let uri = make_uri(path)
.inspect_err(|e| {
tracing::warn!(
"Skipping path in list_resources (make_uri failed): {}: {e}",
path.display()
);
})
.ok()?;
let name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string();
let raw = RawResource::new(uri, name)
.with_mime_type("application/json")
.with_description("LSP diagnostics for this file");
Some(rmcp::model::Annotated::new(raw, None))
})
.collect()
};
Ok(ListResourcesResult::with_all_items(resources))
}
async fn read_resource(
&self,
request: ReadResourceRequestParams,
_context: rmcp::service::RequestContext<RoleServer>,
) -> Result<ReadResourceResult, McpError> {
let path =
parse_uri(&request.uri).map_err(|e| McpError::invalid_params(e.to_string(), None))?;
{
let translator = self.context.translator.lock().await;
translator
.validate_path(&path)
.map_err(|e| McpError::invalid_params(e.to_string(), None))?;
}
let lsp_uri = crate::bridge::path_to_uri(&path);
let diagnostics = {
let translator = self.context.translator.lock().await;
translator
.notification_cache()
.get_diagnostics(lsp_uri.as_str())
.cloned()
};
let json = serde_json::to_string(&diagnostics)
.map_err(|e| McpError::internal_error(format!("Serialization error: {e}"), None))?;
Ok(ReadResourceResult::new(vec![ResourceContents::text(
json,
request.uri,
)]))
}
async fn subscribe(
&self,
request: SubscribeRequestParams,
_context: rmcp::service::RequestContext<RoleServer>,
) -> Result<(), McpError> {
let path =
parse_uri(&request.uri).map_err(|e| McpError::invalid_params(e.to_string(), None))?;
{
let translator = self.context.translator.lock().await;
translator
.validate_path(&path)
.map_err(|e| McpError::invalid_params(e.to_string(), None))?;
}
self.context
.subscriptions
.subscribe(request.uri)
.await
.map_err(|e| McpError::invalid_params(e, None))?;
Ok(())
}
async fn unsubscribe(
&self,
request: UnsubscribeRequestParams,
_context: rmcp::service::RequestContext<RoleServer>,
) -> Result<(), McpError> {
parse_uri(&request.uri).map_err(|e| McpError::invalid_params(e.to_string(), None))?;
self.context.subscriptions.unsubscribe(&request.uri).await;
Ok(())
}
fn get_info(&self) -> ServerInfo {
let mut implementation = Implementation::new("mcpls", env!("CARGO_PKG_VERSION"));
implementation.title = Some("MCPLS - MCP to LSP Bridge".to_string());
implementation.description = Some(env!("CARGO_PKG_DESCRIPTION").to_string());
implementation.website_url = Some("https://github.com/bug-ops/mcpls".to_string());
let capabilities = ServerCapabilities::builder()
.enable_tools()
.enable_resources()
.enable_resources_subscribe()
.build();
let mut server_info = ServerInfo::new(capabilities);
server_info.server_info = implementation;
server_info.instructions = Some(
concat!(
"Universal MCP to LSP bridge. Exposes Language Server Protocol ",
"capabilities as MCP tools for semantic code intelligence. ",
"Supports hover, definition, references, diagnostics, rename, ",
"completions, symbols, and formatting."
)
.to_string(),
);
server_info
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
fn create_test_server() -> McplsServer {
let translator = Arc::new(Mutex::new(Translator::new()));
let subscriptions = Arc::new(ResourceSubscriptions::new());
McplsServer::new(translator, subscriptions)
}
#[tokio::test]
async fn test_server_info() {
let server = create_test_server();
let info = server.get_info();
assert!(info.capabilities.tools.is_some());
assert_eq!(info.server_info.name, "mcpls");
assert!(info.instructions.is_some());
}
#[tokio::test]
async fn test_hover_tool_with_params() {
let server = create_test_server();
let params = Parameters(HoverParams {
file_path: "/nonexistent/file.rs".to_string(),
line: 1,
character: 1,
});
let result = server.get_hover(params).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_definition_tool_with_params() {
let server = create_test_server();
let params = Parameters(DefinitionParams {
file_path: "/test/file.rs".to_string(),
line: 10,
character: 5,
});
let result = server.get_definition(params).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_references_tool_with_params() {
let server = create_test_server();
let params = Parameters(ReferencesParams {
file_path: "/test/file.rs".to_string(),
line: 10,
character: 5,
include_declaration: false,
});
let result = server.get_references(params).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_diagnostics_tool_with_params() {
let server = create_test_server();
let params = Parameters(DiagnosticsParams {
file_path: "/test/file.rs".to_string(),
});
let result = server.get_diagnostics(params).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_rename_tool_with_params() {
let server = create_test_server();
let params = Parameters(RenameParams {
file_path: "/test/file.rs".to_string(),
line: 10,
character: 5,
new_name: "new_name".to_string(),
});
let result = server.rename_symbol(params).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_completions_tool_with_params() {
let server = create_test_server();
let params = Parameters(CompletionsParams {
file_path: "/test/file.rs".to_string(),
line: 10,
character: 5,
trigger: None,
});
let result = server.get_completions(params).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_document_symbols_tool_with_params() {
let server = create_test_server();
let params = Parameters(DocumentSymbolsParams {
file_path: "/test/file.rs".to_string(),
});
let result = server.get_document_symbols(params).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_format_document_tool_with_params() {
let server = create_test_server();
let params = Parameters(FormatDocumentParams {
file_path: "/test/file.rs".to_string(),
tab_size: 4,
insert_spaces: true,
});
let result = server.format_document(params).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_workspace_symbol_search_tool_with_params() {
let server = create_test_server();
let params = Parameters(WorkspaceSymbolParams {
query: "User".to_string(),
kind_filter: None,
limit: 100,
});
let result = server.workspace_symbol_search(params).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_code_actions_tool_with_params() {
let server = create_test_server();
let params = Parameters(CodeActionsParams {
file_path: "/test/file.rs".to_string(),
start_line: 10,
start_character: 5,
end_line: 10,
end_character: 15,
kind_filter: None,
});
let result = server.get_code_actions(params).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_prepare_call_hierarchy_tool_with_params() {
let server = create_test_server();
let params = Parameters(CallHierarchyPrepareParams {
file_path: "/test/file.rs".to_string(),
line: 10,
character: 5,
});
let result = server.prepare_call_hierarchy(params).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_incoming_calls_tool_with_params() {
let server = create_test_server();
let item = serde_json::json!({
"name": "test_function",
"kind": 12,
"uri": "file:///test/file.rs",
"range": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 10}
},
"selectionRange": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 10}
}
});
let params = Parameters(CallHierarchyCallsParams { item });
let result = server.get_incoming_calls(params).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_outgoing_calls_tool_with_params() {
let server = create_test_server();
let item = serde_json::json!({
"name": "test_function",
"kind": 12,
"uri": "file:///test/file.rs",
"range": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 10}
},
"selectionRange": {
"start": {"line": 0, "character": 0},
"end": {"line": 0, "character": 10}
}
});
let params = Parameters(CallHierarchyCallsParams { item });
let result = server.get_outgoing_calls(params).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_cached_diagnostics_tool_with_params() {
use std::fs;
use tempfile::TempDir;
let server = create_test_server();
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test.rs");
fs::write(&test_file, "fn main() {}").unwrap();
let params = Parameters(CachedDiagnosticsParams {
file_path: test_file.to_str().unwrap().to_string(),
});
let result = server.get_cached_diagnostics(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("diagnostics").is_some());
}
#[tokio::test]
async fn test_cached_diagnostics_tool_nonexistent_file() {
let server = create_test_server();
let params = Parameters(CachedDiagnosticsParams {
file_path: "/nonexistent/file.rs".to_string(),
});
let result = server.get_cached_diagnostics(params).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_server_logs_tool_with_default_params() {
let server = create_test_server();
let params = Parameters(ServerLogsParams {
limit: 50,
min_level: None,
});
let result = server.get_server_logs(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("logs").is_some());
}
#[tokio::test]
async fn test_server_logs_tool_with_error_level() {
let server = create_test_server();
let params = Parameters(ServerLogsParams {
limit: 10,
min_level: Some("error".to_string()),
});
let result = server.get_server_logs(params).await;
assert!(result.is_ok());
let json_str = result.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
let logs = parsed.get("logs").unwrap().as_array().unwrap();
assert_eq!(logs.len(), 0);
}
#[tokio::test]
async fn test_server_logs_tool_with_warning_level() {
let server = create_test_server();
let params = Parameters(ServerLogsParams {
limit: 100,
min_level: Some("warning".to_string()),
});
let result = server.get_server_logs(params).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_server_logs_tool_with_info_level() {
let server = create_test_server();
let params = Parameters(ServerLogsParams {
limit: 50,
min_level: Some("info".to_string()),
});
let result = server.get_server_logs(params).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_server_logs_tool_with_debug_level() {
let server = create_test_server();
let params = Parameters(ServerLogsParams {
limit: 20,
min_level: Some("debug".to_string()),
});
let result = server.get_server_logs(params).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_server_logs_tool_with_invalid_level() {
let server = create_test_server();
let params = Parameters(ServerLogsParams {
limit: 10,
min_level: Some("invalid_level".to_string()),
});
let result = server.get_server_logs(params).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_server_logs_tool_with_zero_limit() {
let server = create_test_server();
let params = Parameters(ServerLogsParams {
limit: 0,
min_level: None,
});
let result = server.get_server_logs(params).await;
assert!(result.is_ok());
let json_str = result.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
let logs = parsed.get("logs").unwrap().as_array().unwrap();
assert_eq!(logs.len(), 0);
}
#[tokio::test]
async fn test_server_messages_tool_with_default_params() {
let server = create_test_server();
let params = Parameters(ServerMessagesParams { limit: 20 });
let result = server.get_server_messages(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("messages").is_some());
}
#[tokio::test]
async fn test_server_messages_tool_with_custom_limit() {
let server = create_test_server();
let params = Parameters(ServerMessagesParams { limit: 5 });
let result = server.get_server_messages(params).await;
assert!(result.is_ok());
let json_str = result.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
let messages = parsed.get("messages").unwrap().as_array().unwrap();
assert_eq!(messages.len(), 0);
}
#[tokio::test]
async fn test_server_messages_tool_with_zero_limit() {
let server = create_test_server();
let params = Parameters(ServerMessagesParams { limit: 0 });
let result = server.get_server_messages(params).await;
assert!(result.is_ok());
let json_str = result.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
let messages = parsed.get("messages").unwrap().as_array().unwrap();
assert_eq!(messages.len(), 0);
}
#[tokio::test]
async fn test_server_messages_tool_with_large_limit() {
let server = create_test_server();
let params = Parameters(ServerMessagesParams { limit: 1000 });
let result = server.get_server_messages(params).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_get_signature_help_tool_with_params() {
let server = create_test_server();
let params = Parameters(SignatureHelpParams {
file_path: "/test/file.rs".to_string(),
line: 10,
character: 5,
});
let result = server.get_signature_help(params).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_go_to_implementation_tool_with_params() {
let server = create_test_server();
let params = Parameters(GoToImplementationParams {
file_path: "/test/file.rs".to_string(),
line: 10,
character: 5,
});
let result = server.go_to_implementation(params).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_go_to_type_definition_tool_with_params() {
let server = create_test_server();
let params = Parameters(GoToTypeDefinitionParams {
file_path: "/test/file.rs".to_string(),
line: 10,
character: 5,
});
let result = server.go_to_type_definition(params).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_get_inlay_hints_tool_with_params() {
let server = create_test_server();
let params = Parameters(InlayHintsParams {
file_path: "/test/file.rs".to_string(),
start_line: 1,
start_character: 1,
end_line: 10,
end_character: 1,
});
let result = server.get_inlay_hints(params).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_list_resources_returns_empty_when_no_open_documents() {
let server = create_test_server();
let empty = {
let translator = server.context.translator.lock().await;
translator.document_tracker().open_paths().count() == 0
};
assert!(empty);
}
#[test]
fn test_read_resource_rejects_file_scheme() {
let result = parse_uri("file:///some/file.rs");
assert!(result.is_err());
}
#[test]
fn test_subscribe_rejects_https_scheme() {
let result = parse_uri("https://evil.com/file.rs");
assert!(result.is_err());
}
#[tokio::test]
async fn test_validate_path_rejects_nonexistent_path() {
use std::path::Path;
let translator = Arc::new(Mutex::new(Translator::new()));
let result = {
let t = translator.lock().await;
t.validate_path(Path::new("/this/path/does/not/exist/at/all.rs"))
};
assert!(result.is_err());
}
#[tokio::test]
async fn test_subscription_cap_enforced_in_handler_context() {
use crate::bridge::resources::MAX_SUBSCRIPTIONS;
let subscriptions = Arc::new(ResourceSubscriptions::new());
for i in 0..MAX_SUBSCRIPTIONS {
subscriptions
.subscribe(format!("lsp-diagnostics:///file{i}.rs"))
.await
.unwrap();
}
let over = subscriptions
.subscribe("lsp-diagnostics:///overflow.rs".to_string())
.await;
assert!(over.is_err());
}
#[tokio::test]
async fn test_unsubscribe_nonexistent_is_noop() {
let subscriptions = Arc::new(ResourceSubscriptions::new());
let removed = subscriptions
.unsubscribe("lsp-diagnostics:///nonexistent.rs")
.await;
assert!(!removed);
}
#[tokio::test]
async fn test_server_capabilities_include_resources() {
let server = create_test_server();
let info = server.get_info();
assert!(info.capabilities.resources.is_some());
}
}