use std::collections::HashMap;
use rmcp::{
ErrorData as McpError, RoleServer,
handler::server::{router::tool::ToolRouter, wrapper::Parameters},
model::*,
schemars,
service::RequestContext,
tool, tool_router,
};
use tracing::instrument;
use super::core::{NeovimMcpServer, find_get_all_targets};
use super::lua_tools;
use crate::neovim::client::TypeHierarchyItem;
use crate::neovim::{
CallHierarchyItem, CodeAction, DocumentIdentifier, FormattingOptions, NeovimClient, Position,
PrepareRenameResult, Range, WorkspaceEdit, string_or_struct,
};
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct ConnectNvimRequest {
pub target: String,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct ConnectionRequest {
pub connection_id: String,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct BufferRequest {
pub connection_id: String,
pub id: u64,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct BufferReadRequest {
pub connection_id: String,
#[serde(deserialize_with = "string_or_struct")]
pub document: DocumentIdentifier,
#[serde(default)]
pub start: i64,
#[serde(default = "default_end_line")]
pub end: i64,
}
fn default_end_line() -> i64 {
-1
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct ExecuteLuaRequest {
pub connection_id: String,
pub code: String,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct WaitForLspReadyRequest {
pub connection_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub client_name: Option<String>,
#[serde(default = "default_timeout")]
pub timeout_ms: u64,
}
fn default_timeout() -> u64 {
5000
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct WorkspaceSymbolsParams {
pub connection_id: String,
pub lsp_client_name: String,
pub query: String,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct CodeActionsParams {
pub connection_id: String,
#[serde(deserialize_with = "string_or_struct")]
pub document: DocumentIdentifier,
pub lsp_client_name: String,
pub start_line: u64,
pub start_character: u64,
pub end_line: u64,
pub end_character: u64,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct HoverParam {
pub connection_id: String,
#[serde(deserialize_with = "string_or_struct")]
pub document: DocumentIdentifier,
pub lsp_client_name: String,
#[serde(flatten)]
pub position: Position,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct DocumentSymbolsParams {
pub connection_id: String,
#[serde(deserialize_with = "string_or_struct")]
pub document: DocumentIdentifier,
pub lsp_client_name: String,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct ReferencesParams {
pub connection_id: String,
#[serde(deserialize_with = "string_or_struct")]
pub document: DocumentIdentifier,
pub lsp_client_name: String,
#[serde(flatten)]
pub position: Position,
pub include_declaration: bool,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct DefinitionParams {
pub connection_id: String,
#[serde(deserialize_with = "string_or_struct")]
pub document: DocumentIdentifier,
pub lsp_client_name: String,
#[serde(flatten)]
pub position: Position,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct TypeDefinitionParams {
pub connection_id: String,
#[serde(deserialize_with = "string_or_struct")]
pub document: DocumentIdentifier,
pub lsp_client_name: String,
#[serde(flatten)]
pub position: Position,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct ImplementationParams {
pub connection_id: String,
#[serde(deserialize_with = "string_or_struct")]
pub document: DocumentIdentifier,
pub lsp_client_name: String,
#[serde(flatten)]
pub position: Position,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct DeclarationParams {
pub connection_id: String,
#[serde(deserialize_with = "string_or_struct")]
pub document: DocumentIdentifier,
pub lsp_client_name: String,
#[serde(flatten)]
pub position: Position,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct ResolveCodeActionParams {
pub connection_id: String,
pub lsp_client_name: String,
#[serde(deserialize_with = "string_or_struct")]
pub code_action: CodeAction,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct ApplyWorkspaceEditParams {
pub connection_id: String,
pub lsp_client_name: String,
#[serde(deserialize_with = "string_or_struct")]
pub workspace_edit: WorkspaceEdit,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct RenameParams {
pub connection_id: String,
#[serde(deserialize_with = "string_or_struct")]
pub document: DocumentIdentifier,
pub lsp_client_name: String,
#[serde(flatten)]
pub position: Position,
pub new_name: String,
#[serde(default = "default_prepare_first")]
pub prepare_first: bool,
}
fn default_prepare_first() -> bool {
true
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct DocumentFormattingParams {
pub connection_id: String,
#[serde(deserialize_with = "string_or_struct")]
pub document: DocumentIdentifier,
pub lsp_client_name: String,
pub options: FormattingOptions,
#[serde(default)]
pub apply_edits: bool,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct DocumentRangeFormattingParams {
pub connection_id: String,
#[serde(deserialize_with = "string_or_struct")]
pub document: DocumentIdentifier,
pub lsp_client_name: String,
pub start_line: u64,
pub start_character: u64,
pub end_line: u64,
pub end_character: u64,
#[serde(deserialize_with = "string_or_struct")]
pub options: FormattingOptions,
#[serde(default)]
pub apply_edits: bool,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct LspOrganizeImportsParams {
pub connection_id: String,
#[serde(deserialize_with = "string_or_struct")]
pub document: DocumentIdentifier,
pub lsp_client_name: String,
#[serde(default = "default_true")]
pub apply_edits: bool,
}
fn default_true() -> bool {
true
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct NavigateParams {
pub connection_id: String,
#[serde(deserialize_with = "string_or_struct")]
pub document: DocumentIdentifier,
#[serde(flatten)]
pub position: Position,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct CallHierarchyPrepareParams {
pub connection_id: String,
#[serde(deserialize_with = "string_or_struct")]
pub document: DocumentIdentifier,
pub lsp_client_name: String,
#[serde(flatten)]
pub position: Position,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct CallHierarchyIncomingCallsParams {
pub connection_id: String,
pub lsp_client_name: String,
#[serde(deserialize_with = "string_or_struct")]
pub item: CallHierarchyItem,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct CallHierarchyOutgoingCallsParams {
pub connection_id: String,
pub lsp_client_name: String,
#[serde(deserialize_with = "string_or_struct")]
pub item: CallHierarchyItem,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct TypeHierarchyPrepareParams {
pub connection_id: String,
#[serde(deserialize_with = "string_or_struct")]
pub document: DocumentIdentifier,
pub lsp_client_name: String,
#[serde(flatten)]
pub position: Position,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct TypeHierarchySupertypesParams {
pub connection_id: String,
pub lsp_client_name: String,
#[serde(deserialize_with = "string_or_struct")]
pub item: TypeHierarchyItem,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct TypeHierarchySubtypesParams {
pub connection_id: String,
pub lsp_client_name: String,
#[serde(deserialize_with = "string_or_struct")]
pub item: TypeHierarchyItem,
}
macro_rules! include_files {
($($key:ident),* $(,)?) => {{
let mut map = HashMap::new();
$(
map.insert(stringify!($key), include_str!(concat!("../../docs/tools/", stringify!($key), ".md")));
)*
map
}};
}
impl NeovimMcpServer {
pub fn tool_descriptions() -> HashMap<&'static str, &'static str> {
include_files! {
get_targets,
connect,
read,
buffer_diagnostics,
}
}
pub fn get_tool_extra_description(&self, name: &str) -> Option<String> {
if name == "get_targets" {
Some(self.get_connections_instruction())
} else {
None
}
}
}
#[tool_router]
impl NeovimMcpServer {
#[tool]
#[instrument(skip(self))]
pub async fn get_targets(&self) -> Result<CallToolResult, McpError> {
let targets = find_get_all_targets();
if targets.is_empty() {
return Err(McpError::invalid_request(
"No Neovim targets found".to_string(),
None,
));
}
Ok(CallToolResult::success(vec![Content::json(targets)?]))
}
#[tool]
#[instrument(skip(self))]
pub async fn connect(
&self,
Parameters(ConnectNvimRequest { target: path }): Parameters<ConnectNvimRequest>,
ctx: RequestContext<RoleServer>,
) -> Result<CallToolResult, McpError> {
let connection_id = self.generate_shorter_connection_id(&path);
if let Some(mut old_client) = self.nvim_clients.get_mut(&connection_id) {
let _ = old_client.disconnect().await;
}
let mut client = NeovimClient::default();
client.connect_path(&path).await?;
self.setup_new_client(&connection_id, Box::new(client), &ctx)
.await?;
Ok(CallToolResult::success(vec![Content::json(
serde_json::json!({
"connection_id": connection_id,
}),
)?]))
}
#[tool(description = "Connect via TCP address")]
#[instrument(skip(self))]
pub async fn connect_tcp(
&self,
Parameters(ConnectNvimRequest { target: address }): Parameters<ConnectNvimRequest>,
ctx: RequestContext<RoleServer>,
) -> Result<CallToolResult, McpError> {
let connection_id = self.generate_shorter_connection_id(&address);
if let Some(mut old_client) = self.nvim_clients.get_mut(&connection_id) {
let _ = old_client.disconnect().await;
}
let mut client = NeovimClient::default();
client.connect_tcp(&address).await?;
self.setup_new_client(&connection_id, Box::new(client), &ctx)
.await?;
Ok(CallToolResult::success(vec![Content::json(
serde_json::json!({
"connection_id": connection_id,
}),
)?]))
}
#[tool(description = "Disconnect from Neovim instance")]
#[instrument(skip(self))]
pub async fn disconnect(
&self,
Parameters(ConnectionRequest { connection_id }): Parameters<ConnectionRequest>,
) -> Result<CallToolResult, McpError> {
let target = {
let client = self.get_connection(&connection_id)?;
client.target().unwrap_or_else(|| "Unknown".to_string())
};
if let Some((_, mut client)) = self.nvim_clients.remove(&connection_id) {
if let Err(e) = client.disconnect().await {
return Err(McpError::internal_error(
format!("Failed to disconnect: {e}"),
None,
));
}
Ok(CallToolResult::success(vec![Content::json(
serde_json::json!({
"connection_id": connection_id,
"target": target,
}),
)?]))
} else {
Err(McpError::invalid_request(
format!("No Neovim connection found for ID: {connection_id}"),
None,
))
}
}
#[tool(description = "List all open buffers")]
#[instrument(skip(self))]
pub async fn list_buffers(
&self,
Parameters(ConnectionRequest { connection_id }): Parameters<ConnectionRequest>,
) -> Result<CallToolResult, McpError> {
let client = self.get_connection(&connection_id)?;
let buffers = client.get_buffers().await?;
Ok(CallToolResult::success(vec![Content::json(buffers)?]))
}
#[tool(description = "Execute Lua code")]
#[instrument(skip(self))]
pub async fn exec_lua(
&self,
Parameters(ExecuteLuaRequest {
connection_id,
code,
}): Parameters<ExecuteLuaRequest>,
) -> Result<CallToolResult, McpError> {
let client = self.get_connection(&connection_id)?;
let result = client.execute_lua(&code).await?;
Ok(CallToolResult::success(vec![Content::json(
serde_json::json!({
"result": format!("{:?}", result)
}),
)?]))
}
#[tool(description = "Wait for LSP client to be ready and attached")]
#[instrument(skip(self))]
pub async fn wait_for_lsp_ready(
&self,
Parameters(WaitForLspReadyRequest {
connection_id,
client_name,
timeout_ms,
}): Parameters<WaitForLspReadyRequest>,
) -> Result<CallToolResult, McpError> {
let client = self.get_connection(&connection_id)?;
client
.wait_for_lsp_ready(client_name.as_deref(), timeout_ms)
.await?;
Ok(CallToolResult::success(vec![Content::json(
serde_json::json!({
"message": "LSP client ready",
"client_name": client_name.unwrap_or_else(|| "any".to_string()),
"timeout_ms": timeout_ms
}),
)?]))
}
#[tool]
#[instrument(skip(self))]
pub async fn read(
&self,
Parameters(BufferReadRequest {
connection_id,
document,
start,
end,
}): Parameters<BufferReadRequest>,
) -> Result<CallToolResult, McpError> {
let client = self.get_connection(&connection_id)?;
let text_content = client.read_document(document, start, end).await?;
Ok(CallToolResult::success(vec![Content::text(text_content)]))
}
#[tool]
#[instrument(skip(self))]
pub async fn buffer_diagnostics(
&self,
Parameters(BufferRequest { connection_id, id }): Parameters<BufferRequest>,
) -> Result<CallToolResult, McpError> {
let client = self.get_connection(&connection_id)?;
let diagnostics = client.get_buffer_diagnostics(id).await?;
Ok(CallToolResult::success(vec![Content::json(diagnostics)?]))
}
#[tool(description = "Get workspace LSP clients")]
#[instrument(skip(self))]
pub async fn lsp_clients(
&self,
Parameters(ConnectionRequest { connection_id }): Parameters<ConnectionRequest>,
) -> Result<CallToolResult, McpError> {
let client = self.get_connection(&connection_id)?;
let lsp_clients = client.lsp_get_clients().await?;
Ok(CallToolResult::success(vec![Content::json(lsp_clients)?]))
}
#[tool(description = "Search workspace symbols by query")]
#[instrument(skip(self))]
pub async fn lsp_workspace_symbols(
&self,
Parameters(WorkspaceSymbolsParams {
connection_id,
lsp_client_name,
query,
}): Parameters<WorkspaceSymbolsParams>,
) -> Result<CallToolResult, McpError> {
let client = self.get_connection(&connection_id)?;
let symbols = client
.lsp_workspace_symbols(&lsp_client_name, &query)
.await?;
Ok(CallToolResult::success(vec![Content::json(symbols)?]))
}
#[tool(description = "Get LSP code actions")]
#[instrument(skip(self))]
pub async fn lsp_code_actions(
&self,
Parameters(CodeActionsParams {
connection_id,
document,
lsp_client_name,
start_line,
start_character,
end_line,
end_character,
}): Parameters<CodeActionsParams>,
) -> Result<CallToolResult, McpError> {
let client = self.get_connection(&connection_id)?;
let start = Position {
line: start_line,
character: start_character,
};
let end = Position {
line: end_line,
character: end_character,
};
let range = Range { start, end };
let code_actions = client
.lsp_get_code_actions(&lsp_client_name, document, range)
.await?;
Ok(CallToolResult::success(vec![Content::json(code_actions)?]))
}
#[tool(description = "Get LSP hover information")]
#[instrument(skip(self))]
pub async fn lsp_hover(
&self,
Parameters(HoverParam {
connection_id,
document,
lsp_client_name,
position,
}): Parameters<HoverParam>,
) -> Result<CallToolResult, McpError> {
let client = self.get_connection(&connection_id)?;
let hover = client
.lsp_hover(&lsp_client_name, document, position)
.await?;
Ok(CallToolResult::success(vec![Content::json(hover)?]))
}
#[tool(description = "Get document symbols")]
#[instrument(skip(self))]
pub async fn lsp_document_symbols(
&self,
Parameters(DocumentSymbolsParams {
connection_id,
document,
lsp_client_name,
}): Parameters<DocumentSymbolsParams>,
) -> Result<CallToolResult, McpError> {
let client = self.get_connection(&connection_id)?;
let symbols = client
.lsp_document_symbols(&lsp_client_name, document)
.await?;
Ok(CallToolResult::success(vec![Content::json(symbols)?]))
}
#[tool(description = "Get LSP references")]
#[instrument(skip(self))]
pub async fn lsp_references(
&self,
Parameters(ReferencesParams {
connection_id,
document,
lsp_client_name,
position,
include_declaration,
}): Parameters<ReferencesParams>,
) -> Result<CallToolResult, McpError> {
let client = self.get_connection(&connection_id)?;
let references = client
.lsp_references(&lsp_client_name, document, position, include_declaration)
.await?;
Ok(CallToolResult::success(vec![Content::json(references)?]))
}
#[tool(description = "Get LSP definition")]
#[instrument(skip(self))]
pub async fn lsp_definition(
&self,
Parameters(DefinitionParams {
connection_id,
document,
lsp_client_name,
position,
}): Parameters<DefinitionParams>,
) -> Result<CallToolResult, McpError> {
let client = self.get_connection(&connection_id)?;
let definition = client
.lsp_definition(&lsp_client_name, document, position)
.await?;
Ok(CallToolResult::success(vec![Content::json(definition)?]))
}
#[tool(description = "Get LSP type definition")]
#[instrument(skip(self))]
pub async fn lsp_type_definition(
&self,
Parameters(TypeDefinitionParams {
connection_id,
document,
lsp_client_name,
position,
}): Parameters<TypeDefinitionParams>,
) -> Result<CallToolResult, McpError> {
let client = self.get_connection(&connection_id)?;
let type_definition = client
.lsp_type_definition(&lsp_client_name, document, position)
.await?;
Ok(CallToolResult::success(vec![Content::json(
type_definition,
)?]))
}
#[tool(description = "Get LSP implementation")]
#[instrument(skip(self))]
pub async fn lsp_implementations(
&self,
Parameters(ImplementationParams {
connection_id,
document,
lsp_client_name,
position,
}): Parameters<ImplementationParams>,
) -> Result<CallToolResult, McpError> {
let client = self.get_connection(&connection_id)?;
let implementation = client
.lsp_implementation(&lsp_client_name, document, position)
.await?;
Ok(CallToolResult::success(vec![Content::json(
implementation,
)?]))
}
#[tool(description = "Get LSP declaration")]
#[instrument(skip(self))]
pub async fn lsp_declaration(
&self,
Parameters(DeclarationParams {
connection_id,
document,
lsp_client_name,
position,
}): Parameters<DeclarationParams>,
) -> Result<CallToolResult, McpError> {
let client = self.get_connection(&connection_id)?;
let declaration = client
.lsp_declaration(&lsp_client_name, document, position)
.await?;
Ok(CallToolResult::success(vec![Content::json(declaration)?]))
}
#[tool(description = "Resolve a code action that may have incomplete data")]
#[instrument(skip(self))]
pub async fn lsp_resolve_code_action(
&self,
Parameters(ResolveCodeActionParams {
connection_id,
lsp_client_name,
code_action,
}): Parameters<ResolveCodeActionParams>,
) -> Result<CallToolResult, McpError> {
let client = self.get_connection(&connection_id)?;
let resolved_action = client
.lsp_resolve_code_action(&lsp_client_name, code_action)
.await?;
Ok(CallToolResult::success(vec![Content::json(
resolved_action,
)?]))
}
#[tool(description = "Apply workspace edits using Neovim's LSP utility functions")]
#[instrument(skip(self))]
pub async fn lsp_apply_edit(
&self,
Parameters(ApplyWorkspaceEditParams {
connection_id,
lsp_client_name,
workspace_edit,
}): Parameters<ApplyWorkspaceEditParams>,
) -> Result<CallToolResult, McpError> {
let client = self.get_connection(&connection_id)?;
client
.lsp_apply_workspace_edit(&lsp_client_name, workspace_edit)
.await?;
Ok(CallToolResult::success(vec![Content::text("success")]))
}
#[tool(description = "Rename symbol across workspace using LSP with optional validation")]
#[instrument(skip(self))]
pub async fn lsp_rename(
&self,
Parameters(RenameParams {
connection_id,
document,
lsp_client_name,
position,
new_name,
prepare_first,
}): Parameters<RenameParams>,
) -> Result<CallToolResult, McpError> {
let client = self.get_connection(&connection_id)?;
if prepare_first {
match client
.lsp_prepare_rename(&lsp_client_name, document.clone(), position.clone())
.await
{
Ok(Some(prepare_result)) => {
let prepare_info = match prepare_result {
PrepareRenameResult::Range(range) => {
format!("Range: {:?}", range)
}
PrepareRenameResult::RangeWithPlaceholder { range, placeholder } => {
format!("Range: {:?}, Current name: '{}'", range, placeholder)
}
PrepareRenameResult::DefaultBehavior { .. } => {
"Default behavior enabled".to_string()
}
};
tracing::debug!("Prepare rename successful: {}", prepare_info);
}
Ok(None) => {
return Err(McpError::invalid_request(
"Position is not renameable according to prepare rename".to_string(),
None,
));
}
Err(e) => {
return Err(McpError::invalid_request(
format!("Prepare rename failed: {}", e),
None,
));
}
}
}
let workspace_edit = client
.lsp_rename(&lsp_client_name, document, position, &new_name)
.await?;
if let Some(edit) = workspace_edit {
client
.lsp_apply_workspace_edit(&lsp_client_name, edit)
.await?;
Ok(CallToolResult::success(vec![Content::text(
"Rename completed successfully",
)]))
} else {
Err(McpError::invalid_request(
"Rename operation is not valid at this position".to_string(),
None,
))
}
}
#[tool(description = "Format entire document using LSP with optional auto-apply")]
#[instrument(skip(self))]
pub async fn lsp_formatting(
&self,
Parameters(DocumentFormattingParams {
connection_id,
document,
lsp_client_name,
options,
apply_edits,
}): Parameters<DocumentFormattingParams>,
) -> Result<CallToolResult, McpError> {
let client = self.get_connection(&connection_id)?;
let text_edits = client
.lsp_formatting(&lsp_client_name, document.clone(), options)
.await?;
if apply_edits {
client
.lsp_apply_text_edits(&lsp_client_name, document, text_edits)
.await?;
Ok(CallToolResult::success(vec![Content::text(
"Formatting applied successfully",
)]))
} else {
Ok(CallToolResult::success(vec![Content::json(text_edits)?]))
}
}
#[tool(
description = "Format a specific range in a document using LSP with optional auto-apply"
)]
#[instrument(skip(self))]
pub async fn lsp_range_formatting(
&self,
Parameters(DocumentRangeFormattingParams {
connection_id,
document,
lsp_client_name,
start_line,
start_character,
end_line,
end_character,
options,
apply_edits,
}): Parameters<DocumentRangeFormattingParams>,
) -> Result<CallToolResult, McpError> {
let client = self.get_connection(&connection_id)?;
let start = Position {
line: start_line,
character: start_character,
};
let end = Position {
line: end_line,
character: end_character,
};
let range = Range { start, end };
let text_edits = client
.lsp_range_formatting(&lsp_client_name, document.clone(), range, options)
.await?;
if apply_edits {
client
.lsp_apply_text_edits(&lsp_client_name, document, text_edits)
.await?;
Ok(CallToolResult::success(vec![Content::text(
"Range formatting applied successfully",
)]))
} else {
Ok(CallToolResult::success(vec![Content::json(text_edits)?]))
}
}
#[tool(description = "Sort and organize imports")]
#[instrument(skip(self))]
pub async fn lsp_organize_imports(
&self,
Parameters(LspOrganizeImportsParams {
connection_id,
document,
lsp_client_name,
apply_edits,
}): Parameters<LspOrganizeImportsParams>,
) -> Result<CallToolResult, McpError> {
let client = self.get_connection(&connection_id)?;
let code_actions = client
.lsp_get_organize_imports_actions(&lsp_client_name, document)
.await?;
if code_actions.is_empty() {
return Ok(CallToolResult::success(vec![Content::text(
"No organize imports actions available for this document",
)]));
}
if !apply_edits {
return Ok(CallToolResult::success(vec![Content::json(code_actions)?]));
}
let action = code_actions[0].clone();
let resolved_action = if action.has_edit() {
action
} else {
client
.lsp_resolve_code_action(&lsp_client_name, action)
.await?
};
if let Some(edit) = resolved_action.edit() {
client
.lsp_apply_workspace_edit(&lsp_client_name, edit.clone())
.await?;
Ok(CallToolResult::success(vec![Content::text(
"Imports organized successfully",
)]))
} else {
Err(McpError::invalid_request(
"Organize imports action does not contain workspace edit".to_string(),
None,
))
}
}
#[tool(
description = "Get the current cursor position: buffer id, buffer name, window id, and zero-based row/col index"
)]
#[instrument(skip(self))]
pub async fn cursor_position(
&self,
Parameters(ConnectionRequest { connection_id }): Parameters<ConnectionRequest>,
) -> Result<CallToolResult, McpError> {
let client = self.get_connection(&connection_id)?;
let lua_code = include_str!("./lua/cursor_position.lua");
let result = client.execute_lua(lua_code).await?;
let json_result = lua_tools::convert_nvim_value_to_json(result).map_err(|e| {
McpError::internal_error(
format!("Failed to convert cursor position result to JSON: {}", e),
None,
)
})?;
Ok(CallToolResult::success(vec![Content::json(json_result)?]))
}
#[tool(
description = "Navigate to a specific position in the current buffer or open a file at a specific position"
)]
#[instrument(skip(self))]
pub async fn navigate(
&self,
Parameters(NavigateParams {
connection_id,
document,
position,
}): Parameters<NavigateParams>,
) -> Result<CallToolResult, McpError> {
let client = self.get_connection(&connection_id)?;
let result = client.navigate(document, position).await?;
Ok(CallToolResult::success(vec![Content::json(result)?]))
}
#[tool(description = "Prepare call hierarchy for a symbol at a specific position")]
#[instrument(skip(self))]
pub async fn lsp_call_hierarchy_prepare(
&self,
Parameters(CallHierarchyPrepareParams {
connection_id,
document,
lsp_client_name,
position,
}): Parameters<CallHierarchyPrepareParams>,
) -> Result<CallToolResult, McpError> {
let client = self.get_connection(&connection_id)?;
let result = client
.lsp_call_hierarchy_prepare(&lsp_client_name, document, position)
.await?;
Ok(CallToolResult::success(vec![Content::json(result)?]))
}
#[tool(description = "Get incoming calls for a call hierarchy item")]
#[instrument(skip(self))]
pub async fn lsp_call_hierarchy_incoming_calls(
&self,
Parameters(CallHierarchyIncomingCallsParams {
connection_id,
lsp_client_name,
item,
}): Parameters<CallHierarchyIncomingCallsParams>,
) -> Result<CallToolResult, McpError> {
let client = self.get_connection(&connection_id)?;
let result = client
.lsp_call_hierarchy_incoming_calls(&lsp_client_name, item)
.await?;
Ok(CallToolResult::success(vec![Content::json(result)?]))
}
#[tool(description = "Get outgoing calls for a call hierarchy item")]
#[instrument(skip(self))]
pub async fn lsp_call_hierarchy_outgoing_calls(
&self,
Parameters(CallHierarchyOutgoingCallsParams {
connection_id,
lsp_client_name,
item,
}): Parameters<CallHierarchyOutgoingCallsParams>,
) -> Result<CallToolResult, McpError> {
let client = self.get_connection(&connection_id)?;
let result = client
.lsp_call_hierarchy_outgoing_calls(&lsp_client_name, item)
.await?;
Ok(CallToolResult::success(vec![Content::json(result)?]))
}
#[tool(description = "Prepare type hierarchy for a symbol at a specific position")]
#[instrument(skip(self))]
pub async fn lsp_type_hierarchy_prepare(
&self,
Parameters(TypeHierarchyPrepareParams {
connection_id,
document,
lsp_client_name,
position,
}): Parameters<TypeHierarchyPrepareParams>,
) -> Result<CallToolResult, McpError> {
let client = self.get_connection(&connection_id)?;
let result = client
.lsp_type_hierarchy_prepare(&lsp_client_name, document, position)
.await?;
Ok(CallToolResult::success(vec![Content::json(result)?]))
}
#[tool(description = "Get supertypes for a type hierarchy item")]
#[instrument(skip(self))]
pub async fn lsp_type_hierarchy_supertypes(
&self,
Parameters(TypeHierarchySupertypesParams {
connection_id,
lsp_client_name,
item,
}): Parameters<TypeHierarchySupertypesParams>,
) -> Result<CallToolResult, McpError> {
let client = self.get_connection(&connection_id)?;
let result = client
.lsp_type_hierarchy_supertypes(&lsp_client_name, item)
.await?;
Ok(CallToolResult::success(vec![Content::json(result)?]))
}
#[tool(description = "Get subtypes for a type hierarchy item")]
#[instrument(skip(self))]
pub async fn lsp_type_hierarchy_subtypes(
&self,
Parameters(TypeHierarchySubtypesParams {
connection_id,
lsp_client_name,
item,
}): Parameters<TypeHierarchySubtypesParams>,
) -> Result<CallToolResult, McpError> {
let client = self.get_connection(&connection_id)?;
let result = client
.lsp_type_hierarchy_subtypes(&lsp_client_name, item)
.await?;
Ok(CallToolResult::success(vec![Content::json(result)?]))
}
}
pub fn build_tool_router() -> ToolRouter<NeovimMcpServer> {
NeovimMcpServer::tool_router()
}