lsp-mcp 0.1.0

MCP server providing unified access to Language Server Protocol features
Documentation
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;

/// LSP MCP Server - provides LSP features via MCP tools
#[derive(Clone)]
pub struct LspMcpServer {
    manager: Arc<LanguageServerManager>,
    tool_router: ToolRouter<Self>,
}

impl LspMcpServer {
    /// Create a new LSP MCP server
    pub fn new() -> Self {
        Self {
            manager: Arc::new(LanguageServerManager::new()),
            tool_router: Self::tool_router(),
        }
    }

    /// Get the language server manager
    pub fn manager(&self) -> &LanguageServerManager {
        &self.manager
    }

    /// Shutdown all language servers
    pub fn shutdown(&self) -> crate::error::Result<()> {
        self.manager.shutdown_all()
    }
}

impl Default for LspMcpServer {
    fn default() -> Self {
        Self::new()
    }
}

// ============================================================================
// Tool Input Schemas
// ============================================================================

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ActivateWorkspaceInput {
    /// Absolute path to the workspace root directory
    pub workspace_path: String,
    /// Languages to activate (e.g., ["rust", "typescript"]). If empty, auto-detects from project files.
    #[serde(default)]
    pub languages: Option<Vec<String>>,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct DeactivateWorkspaceInput {
    /// Absolute path to the workspace root directory
    pub workspace_path: String,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct FilePositionInput {
    /// Absolute path to the file
    pub file_path: String,
    /// 0-indexed line number
    pub line: u32,
    /// 0-indexed character position (column)
    pub character: u32,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct HoverInput {
    /// Absolute path to the file
    pub file_path: String,
    /// 0-indexed line number
    pub line: u32,
    /// 0-indexed character position (column)
    pub character: u32,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ReferencesInput {
    /// Absolute path to the file
    pub file_path: String,
    /// 0-indexed line number
    pub line: u32,
    /// 0-indexed character position
    pub character: u32,
    /// Include the definition in the results (default: false)
    #[serde(default)]
    pub include_declaration: bool,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct CompletionInput {
    /// Absolute path to the file
    pub file_path: String,
    /// 0-indexed line number
    pub line: u32,
    /// 0-indexed character position
    pub character: u32,
    /// Text that triggered the completion (optional, e.g., "." or "::")
    pub trigger_character: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct FileInput {
    /// Absolute path to the file
    pub file_path: String,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct WorkspaceSymbolsInput {
    /// Search query (symbol name or pattern)
    pub query: String,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct RenameInput {
    /// Absolute path to the file
    pub file_path: String,
    /// 0-indexed line number
    pub line: u32,
    /// 0-indexed character position
    pub character: u32,
    /// New name for the symbol
    pub new_name: String,
}

// ============================================================================
// Tool Implementations
// ============================================================================

#[tool_router]
impl LspMcpServer {
    /// Activate a workspace directory and start the appropriate language server(s).
    #[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
            ))])),
        }
    }

    /// List all currently active workspaces and their language servers.
    #[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))?]))
    }

    /// Stop all language servers for a workspace and release resources.
    #[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
            ))])),
        }
    }

    /// Get hover information (type info, documentation) for a symbol at a specific position.
    #[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
            ))])),
        }
    }

    /// Find the definition location(s) of a symbol at the specified position.
    #[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
            ))])),
        }
    }

    /// Find all references to a symbol at the specified position across the workspace.
    #[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
            ))])),
        }
    }

    /// Get code completion suggestions at a position.
    #[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
            ))])),
        }
    }

    /// Get diagnostics (errors, warnings, hints) for a file.
    #[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
            ))])),
        }
    }

    /// Get all symbols defined in a file.
    #[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
            ))])),
        }
    }

    /// Search for symbols across the workspace by name pattern.
    #[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
            ))])),
        }
    }

    /// Check if a symbol can be renamed and get its current name/range.
    #[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
            ))])),
        }
    }

    /// Rename a symbol across the workspace. Returns edits for review, does NOT apply them.
    #[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
            ))])),
        }
    }
}

// ============================================================================
// Server Handler Implementation
// ============================================================================

#[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()
        }
    }
}