use crate::lsp::LanguageServerManager;
use crate::tools;
use rmcp::handler::server::router::tool::ToolRouter;
use rmcp::handler::server::wrapper::Parameters;
use rmcp::model::{CallToolResult, Content, ServerCapabilities, ServerInfo};
use schemars::JsonSchema;
use rmcp::{tool, tool_handler, tool_router, ErrorData as McpError, ServerHandler};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Clone)]
pub struct LspMcpServer {
manager: Arc<LanguageServerManager>,
tool_router: ToolRouter<Self>,
}
impl LspMcpServer {
pub fn new() -> Self {
Self {
manager: Arc::new(LanguageServerManager::new()),
tool_router: Self::tool_router(),
}
}
pub fn manager(&self) -> &LanguageServerManager {
&self.manager
}
pub fn shutdown(&self) -> crate::error::Result<()> {
self.manager.shutdown_all()
}
}
impl Default for LspMcpServer {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ActivateWorkspaceInput {
pub workspace_path: String,
#[serde(default)]
pub languages: Option<Vec<String>>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct DeactivateWorkspaceInput {
pub workspace_path: String,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct FilePositionInput {
pub file_path: String,
pub line: u32,
pub character: u32,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct HoverInput {
pub file_path: String,
pub line: u32,
pub character: u32,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ReferencesInput {
pub file_path: String,
pub line: u32,
pub character: u32,
#[serde(default)]
pub include_declaration: bool,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CompletionInput {
pub file_path: String,
pub line: u32,
pub character: u32,
pub trigger_character: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct FileInput {
pub file_path: String,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct WorkspaceSymbolsInput {
pub query: String,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct RenameInput {
pub file_path: String,
pub line: u32,
pub character: u32,
pub new_name: String,
}
#[tool_router]
impl LspMcpServer {
#[tool(description = "Activate a workspace directory and start the appropriate language server(s). Must be called before using other LSP tools on a project.")]
async fn lsp_activate_workspace(
&self,
Parameters(input): Parameters<ActivateWorkspaceInput>,
) -> Result<CallToolResult, McpError> {
match tools::workspace::activate_workspace(
&self.manager,
&input.workspace_path,
input.languages,
) {
Ok(result) => Ok(CallToolResult::success(vec![Content::json(&result)
.map_err(|e| McpError::internal_error(e.to_string(), None))?])),
Err(e) => Ok(CallToolResult::error(vec![Content::text(format!(
"Error: {}",
e
))])),
}
}
#[tool(description = "List all currently active workspaces and their language servers.")]
async fn lsp_list_workspaces(&self) -> Result<CallToolResult, McpError> {
let result = tools::workspace::list_workspaces(&self.manager);
Ok(CallToolResult::success(vec![Content::json(&result)
.map_err(|e| McpError::internal_error(e.to_string(), None))?]))
}
#[tool(description = "Stop all language servers for a workspace and release resources.")]
async fn lsp_deactivate_workspace(
&self,
Parameters(input): Parameters<DeactivateWorkspaceInput>,
) -> Result<CallToolResult, McpError> {
match tools::workspace::deactivate_workspace(&self.manager, &input.workspace_path) {
Ok(msg) => Ok(CallToolResult::success(vec![Content::text(msg)])),
Err(e) => Ok(CallToolResult::error(vec![Content::text(format!(
"Error: {}",
e
))])),
}
}
#[tool(description = "Get hover information (type info, documentation) for a symbol at a specific position in a file. Returns markdown-formatted documentation and type signatures.")]
async fn lsp_hover(
&self,
Parameters(input): Parameters<HoverInput>,
) -> Result<CallToolResult, McpError> {
match tools::hover::hover(&self.manager, &input.file_path, input.line, input.character) {
Ok(Some(result)) => Ok(CallToolResult::success(vec![Content::json(&result)
.map_err(|e| McpError::internal_error(e.to_string(), None))?])),
Ok(None) => Ok(CallToolResult::success(vec![Content::text(
"No hover information available",
)])),
Err(e) => Ok(CallToolResult::error(vec![Content::text(format!(
"Error: {}",
e
))])),
}
}
#[tool(description = "Find the definition location(s) of a symbol at the specified position. Returns file paths and positions where the symbol is defined.")]
async fn lsp_goto_definition(
&self,
Parameters(input): Parameters<FilePositionInput>,
) -> Result<CallToolResult, McpError> {
match tools::definition::goto_definition(
&self.manager,
&input.file_path,
input.line,
input.character,
) {
Ok(result) => Ok(CallToolResult::success(vec![Content::json(&result)
.map_err(|e| McpError::internal_error(e.to_string(), None))?])),
Err(e) => Ok(CallToolResult::error(vec![Content::text(format!(
"Error: {}",
e
))])),
}
}
#[tool(description = "Find all references to a symbol at the specified position across the workspace. Returns locations where the symbol is used.")]
async fn lsp_find_references(
&self,
Parameters(input): Parameters<ReferencesInput>,
) -> Result<CallToolResult, McpError> {
match tools::references::find_references(
&self.manager,
&input.file_path,
input.line,
input.character,
input.include_declaration,
) {
Ok(result) => Ok(CallToolResult::success(vec![Content::json(&result)
.map_err(|e| McpError::internal_error(e.to_string(), None))?])),
Err(e) => Ok(CallToolResult::error(vec![Content::text(format!(
"Error: {}",
e
))])),
}
}
#[tool(description = "Get code completion suggestions at a position. Returns a list of completion items with labels, kinds, and documentation.")]
async fn lsp_completion(
&self,
Parameters(input): Parameters<CompletionInput>,
) -> Result<CallToolResult, McpError> {
match tools::completion::completion(
&self.manager,
&input.file_path,
input.line,
input.character,
input.trigger_character,
) {
Ok(result) => Ok(CallToolResult::success(vec![Content::json(&result)
.map_err(|e| McpError::internal_error(e.to_string(), None))?])),
Err(e) => Ok(CallToolResult::error(vec![Content::text(format!(
"Error: {}",
e
))])),
}
}
#[tool(description = "Get diagnostics (errors, warnings, hints) for a file. Returns a list of diagnostic messages with severity, location, and message text.")]
async fn lsp_diagnostics(
&self,
Parameters(input): Parameters<FileInput>,
) -> Result<CallToolResult, McpError> {
match tools::diagnostics::get_diagnostics(&self.manager, &input.file_path) {
Ok(result) => Ok(CallToolResult::success(vec![Content::json(&result)
.map_err(|e| McpError::internal_error(e.to_string(), None))?])),
Err(e) => Ok(CallToolResult::error(vec![Content::text(format!(
"Error: {}",
e
))])),
}
}
#[tool(description = "Get all symbols (functions, classes, variables, etc.) defined in a file. Returns a hierarchical tree of symbols with their names, kinds, and locations.")]
async fn lsp_document_symbols(
&self,
Parameters(input): Parameters<FileInput>,
) -> Result<CallToolResult, McpError> {
match tools::symbols::document_symbols(&self.manager, &input.file_path) {
Ok(result) => Ok(CallToolResult::success(vec![Content::json(&result)
.map_err(|e| McpError::internal_error(e.to_string(), None))?])),
Err(e) => Ok(CallToolResult::error(vec![Content::text(format!(
"Error: {}",
e
))])),
}
}
#[tool(description = "Search for symbols across the entire workspace by name pattern. Useful for finding types, functions, or classes by name.")]
async fn lsp_workspace_symbols(
&self,
Parameters(input): Parameters<WorkspaceSymbolsInput>,
) -> Result<CallToolResult, McpError> {
match tools::symbols::workspace_symbols(&self.manager, &input.query) {
Ok(result) => Ok(CallToolResult::success(vec![Content::json(&result)
.map_err(|e| McpError::internal_error(e.to_string(), None))?])),
Err(e) => Ok(CallToolResult::error(vec![Content::text(format!(
"Error: {}",
e
))])),
}
}
#[tool(description = "Check if a symbol can be renamed and get its current name/range.")]
async fn lsp_prepare_rename(
&self,
Parameters(input): Parameters<FilePositionInput>,
) -> Result<CallToolResult, McpError> {
match tools::rename::prepare_rename(
&self.manager,
&input.file_path,
input.line,
input.character,
) {
Ok(result) => Ok(CallToolResult::success(vec![Content::json(&result)
.map_err(|e| McpError::internal_error(e.to_string(), None))?])),
Err(e) => Ok(CallToolResult::error(vec![Content::text(format!(
"Error: {}",
e
))])),
}
}
#[tool(description = "Rename a symbol across the workspace. Returns the text edits needed to rename the symbol. Does NOT apply changes - returns edits for review.")]
async fn lsp_rename_symbol(
&self,
Parameters(input): Parameters<RenameInput>,
) -> Result<CallToolResult, McpError> {
match tools::rename::rename_symbol(
&self.manager,
&input.file_path,
input.line,
input.character,
&input.new_name,
) {
Ok(result) => Ok(CallToolResult::success(vec![Content::json(&result)
.map_err(|e| McpError::internal_error(e.to_string(), None))?])),
Err(e) => Ok(CallToolResult::error(vec![Content::text(format!(
"Error: {}",
e
))])),
}
}
}
#[tool_handler]
impl ServerHandler for LspMcpServer {
fn get_info(&self) -> ServerInfo {
ServerInfo {
capabilities: ServerCapabilities::builder().enable_tools().build(),
server_info: rmcp::model::Implementation::from_build_env(),
instructions: Some("LSP MCP Server providing IDE features for multiple programming languages. Call lsp_activate_workspace first to start language servers for a project.".to_string()),
..Default::default()
}
}
}