nvim_mcp/server/
tools.rs

1use std::collections::HashMap;
2
3use rmcp::{
4    ErrorData as McpError, RoleServer,
5    handler::server::{router::tool::ToolRouter, wrapper::Parameters},
6    model::*,
7    schemars,
8    service::RequestContext,
9    tool, tool_router,
10};
11use tracing::instrument;
12
13use super::core::{NeovimMcpServer, find_get_all_targets};
14use super::lua_tools;
15use crate::neovim::client::TypeHierarchyItem;
16use crate::neovim::{
17    CallHierarchyItem, CodeAction, DocumentIdentifier, FormattingOptions, NeovimClient, Position,
18    PrepareRenameResult, Range, WorkspaceEdit, string_or_struct,
19};
20
21/// Connect to Neovim instance via unix socket or TCP
22#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
23pub struct ConnectNvimRequest {
24    /// target can be a unix socket path or a TCP address
25    pub target: String,
26}
27
28/// New parameter struct for connection-aware requests
29#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
30pub struct ConnectionRequest {
31    /// Unique identifier for the target Neovim instance
32    pub connection_id: String,
33}
34
35/// Updated parameter struct for buffer operations
36#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
37pub struct BufferRequest {
38    /// Unique identifier for the target Neovim instance
39    pub connection_id: String,
40    /// Neovim Buffer ID
41    pub id: u64,
42}
43
44/// Buffer read request parameters
45#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
46pub struct BufferReadRequest {
47    /// Unique identifier for the target Neovim instance
48    pub connection_id: String,
49    /// Universal document identifier
50    // Supports both string and struct deserialization.
51    // Compatible with Claude Code when using subscription.
52    #[serde(deserialize_with = "string_or_struct")]
53    pub document: DocumentIdentifier,
54    /// Start line index (zero-based, optional - defaults to 0)
55    #[serde(default)]
56    pub start: i64,
57    /// End line index, exclusive (zero-based, optional - defaults to -1 for end of buffer)
58    #[serde(default = "default_end_line")]
59    pub end: i64,
60}
61
62fn default_end_line() -> i64 {
63    -1
64}
65
66/// Lua execution request
67#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
68pub struct ExecuteLuaRequest {
69    /// Unique identifier for the target Neovim instance
70    pub connection_id: String,
71    /// Lua code to execute in Neovim
72    pub code: String,
73}
74
75/// Wait for LSP client readiness parameters
76#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
77pub struct WaitForLspReadyRequest {
78    /// Unique identifier for the target Neovim instance
79    pub connection_id: String,
80    /// Optional specific LSP client name to wait for (waits for any client if None)
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub client_name: Option<String>,
83    /// Timeout in milliseconds (default 5000ms)
84    #[serde(default = "default_timeout")]
85    pub timeout_ms: u64,
86}
87
88fn default_timeout() -> u64 {
89    5000
90}
91
92/// Workspace symbols parameters
93#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
94pub struct WorkspaceSymbolsParams {
95    /// Unique identifier for the target Neovim instance
96    pub connection_id: String,
97    /// Lsp client name
98    pub lsp_client_name: String,
99    /// A query string to filter symbols by. Clients may send an empty string here to request all symbols.
100    pub query: String,
101}
102
103/// Code Actions parameters
104#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
105pub struct CodeActionsParams {
106    /// Unique identifier for the target Neovim instance
107    pub connection_id: String,
108    /// Universal document identifier
109    // Supports both string and struct deserialization.
110    // Compatible with Claude Code when using subscription.
111    #[serde(deserialize_with = "string_or_struct")]
112    pub document: DocumentIdentifier,
113    /// Lsp client name
114    pub lsp_client_name: String,
115    /// Range start position, line number starts from 0
116    pub start_line: u64,
117    /// Range start position, character number starts from 0
118    pub start_character: u64,
119    /// Range end position, line number starts from 0
120    pub end_line: u64,
121    /// Range end position, character number starts from 0
122    pub end_character: u64,
123}
124
125/// Hover parameters
126#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
127pub struct HoverParam {
128    /// Unique identifier for the target Neovim instance
129    pub connection_id: String,
130    /// Universal document identifier
131    // Supports both string and struct deserialization.
132    // Compatible with Claude Code when using subscription.
133    #[serde(deserialize_with = "string_or_struct")]
134    pub document: DocumentIdentifier,
135    /// Lsp client name
136    pub lsp_client_name: String,
137    /// Symbol position (zero-based)
138    #[serde(flatten)]
139    pub position: Position,
140}
141
142/// Document symbols parameters
143#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
144pub struct DocumentSymbolsParams {
145    /// Unique identifier for the target Neovim instance
146    pub connection_id: String,
147    /// Universal document identifier
148    // Supports both string and struct deserialization.
149    // Compatible with Claude Code when using subscription.
150    #[serde(deserialize_with = "string_or_struct")]
151    pub document: DocumentIdentifier,
152    /// Lsp client name
153    pub lsp_client_name: String,
154}
155
156/// References parameters
157#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
158pub struct ReferencesParams {
159    /// Unique identifier for the target Neovim instance
160    pub connection_id: String,
161    /// Universal document identifier
162    // Supports both string and struct deserialization.
163    // Compatible with Claude Code when using subscription.
164    #[serde(deserialize_with = "string_or_struct")]
165    pub document: DocumentIdentifier,
166    /// Lsp client name
167    pub lsp_client_name: String,
168    /// Symbol position (zero-based)
169    #[serde(flatten)]
170    pub position: Position,
171    /// Include the declaration of the current symbol in the results
172    pub include_declaration: bool,
173}
174
175/// Definition parameters
176#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
177pub struct DefinitionParams {
178    /// Unique identifier for the target Neovim instance
179    pub connection_id: String,
180    /// Universal document identifier
181    // Supports both string and struct deserialization.
182    // Compatible with Claude Code when using subscription.
183    #[serde(deserialize_with = "string_or_struct")]
184    pub document: DocumentIdentifier,
185    /// Lsp client name
186    pub lsp_client_name: String,
187    /// Symbol position (zero-based)
188    #[serde(flatten)]
189    pub position: Position,
190}
191
192/// Type definition parameters
193#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
194pub struct TypeDefinitionParams {
195    /// Unique identifier for the target Neovim instance
196    pub connection_id: String,
197    /// Universal document identifier
198    // Supports both string and struct deserialization.
199    // Compatible with Claude Code when using subscription.
200    #[serde(deserialize_with = "string_or_struct")]
201    pub document: DocumentIdentifier,
202    /// Lsp client name
203    pub lsp_client_name: String,
204    /// Symbol position (zero-based)
205    #[serde(flatten)]
206    pub position: Position,
207}
208
209/// Implementation parameters
210#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
211pub struct ImplementationParams {
212    /// Unique identifier for the target Neovim instance
213    pub connection_id: String,
214    /// Universal document identifier
215    // Supports both string and struct deserialization.
216    // Compatible with Claude Code when using subscription.
217    #[serde(deserialize_with = "string_or_struct")]
218    pub document: DocumentIdentifier,
219    /// Lsp client name
220    pub lsp_client_name: String,
221    /// Symbol position (zero-based)
222    #[serde(flatten)]
223    pub position: Position,
224}
225
226/// Declaration parameters
227#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
228pub struct DeclarationParams {
229    /// Unique identifier for the target Neovim instance
230    pub connection_id: String,
231    /// Universal document identifier
232    // Supports both string and struct deserialization.
233    // Compatible with Claude Code when using subscription.
234    #[serde(deserialize_with = "string_or_struct")]
235    pub document: DocumentIdentifier,
236    /// Lsp client name
237    pub lsp_client_name: String,
238    /// Symbol position (zero-based)
239    #[serde(flatten)]
240    pub position: Position,
241}
242
243/// Code action resolve parameters
244#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
245pub struct ResolveCodeActionParams {
246    /// Unique identifier for the target Neovim instance
247    pub connection_id: String,
248    /// Lsp client name
249    pub lsp_client_name: String,
250    /// Code action to resolve
251    // Supports both string and struct deserialization.
252    // Compatible with Claude Code when using subscription.
253    #[serde(deserialize_with = "string_or_struct")]
254    pub code_action: CodeAction,
255}
256
257/// Apply workspace edit parameters
258#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
259pub struct ApplyWorkspaceEditParams {
260    /// Unique identifier for the target Neovim instance
261    pub connection_id: String,
262    /// Lsp client name
263    pub lsp_client_name: String,
264    /// Workspace edit to apply
265    // Supports both string and struct deserialization.
266    // Compatible with Claude Code when using subscription.
267    #[serde(deserialize_with = "string_or_struct")]
268    pub workspace_edit: WorkspaceEdit,
269}
270
271/// Rename parameters
272#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
273pub struct RenameParams {
274    /// Unique identifier for the target Neovim instance
275    pub connection_id: String,
276    /// Universal document identifier
277    // Supports both string and struct deserialization.
278    // Compatible with Claude Code when using subscription.
279    #[serde(deserialize_with = "string_or_struct")]
280    pub document: DocumentIdentifier,
281    /// Lsp client name
282    pub lsp_client_name: String,
283    /// Symbol position (zero-based)
284    #[serde(flatten)]
285    pub position: Position,
286    /// The new name of the symbol
287    pub new_name: String,
288    /// Whether to run prepare rename first to validate the position (default: true)
289    #[serde(default = "default_prepare_first")]
290    pub prepare_first: bool,
291}
292
293fn default_prepare_first() -> bool {
294    true
295}
296
297/// Document formatting parameters
298#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
299pub struct DocumentFormattingParams {
300    /// Unique identifier for the target Neovim instance
301    pub connection_id: String,
302    /// Universal document identifier
303    // Supports both string and struct deserialization.
304    // Compatible with Claude Code when using subscription.
305    #[serde(deserialize_with = "string_or_struct")]
306    pub document: DocumentIdentifier,
307    /// Lsp client name
308    pub lsp_client_name: String,
309    /// The formatting options
310    pub options: FormattingOptions,
311    /// Whether to apply the text edits automatically (default: false)
312    #[serde(default)]
313    pub apply_edits: bool,
314}
315
316/// Document range formatting parameters
317#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
318pub struct DocumentRangeFormattingParams {
319    /// Unique identifier for the target Neovim instance
320    pub connection_id: String,
321    /// Universal document identifier
322    // Supports both string and struct deserialization.
323    // Compatible with Claude Code when using subscription.
324    #[serde(deserialize_with = "string_or_struct")]
325    pub document: DocumentIdentifier,
326    /// Lsp client name
327    pub lsp_client_name: String,
328    /// Range start position, line number starts from 0
329    pub start_line: u64,
330    /// Range start position, character number starts from 0
331    pub start_character: u64,
332    /// Range end position, line number starts from 0
333    pub end_line: u64,
334    /// Range end position, character number starts from 0
335    pub end_character: u64,
336    /// The formatting options
337    #[serde(deserialize_with = "string_or_struct")]
338    pub options: FormattingOptions,
339    /// Whether to apply the text edits automatically (default: false)
340    #[serde(default)]
341    pub apply_edits: bool,
342}
343
344/// Organize imports parameters
345#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
346pub struct LspOrganizeImportsParams {
347    /// Unique identifier for the target Neovim instance
348    pub connection_id: String,
349    /// Universal document identifier
350    // Supports both string and struct deserialization.
351    // Compatible with Claude Code when using subscription.
352    #[serde(deserialize_with = "string_or_struct")]
353    pub document: DocumentIdentifier,
354    /// Lsp client name
355    pub lsp_client_name: String,
356    /// Whether to apply the text edits automatically (default: true)
357    #[serde(default = "default_true")]
358    pub apply_edits: bool,
359}
360
361fn default_true() -> bool {
362    true
363}
364
365/// Navigate to a specific position in the current buffer or open a file at a specific position
366#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
367pub struct NavigateParams {
368    /// Unique identifier for the target Neovim instance
369    pub connection_id: String,
370    /// Document to navigate to
371    // Supports both string and struct deserialization.
372    // Compatible with Claude Code when using subscription.
373    #[serde(deserialize_with = "string_or_struct")]
374    pub document: DocumentIdentifier,
375    /// Symbol position (zero-based)
376    #[serde(flatten)]
377    pub position: Position,
378}
379
380/// Call hierarchy prepare parameters
381#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
382pub struct CallHierarchyPrepareParams {
383    /// Unique identifier for the target Neovim instance
384    pub connection_id: String,
385    /// Universal document identifier
386    // Supports both string and struct deserialization.
387    // Compatible with Claude Code when using subscription.
388    #[serde(deserialize_with = "string_or_struct")]
389    pub document: DocumentIdentifier,
390    /// Lsp client name
391    pub lsp_client_name: String,
392    /// Symbol position (zero-based)
393    #[serde(flatten)]
394    pub position: Position,
395}
396
397/// Call hierarchy incoming calls parameters
398#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
399pub struct CallHierarchyIncomingCallsParams {
400    /// Unique identifier for the target Neovim instance
401    pub connection_id: String,
402    /// Lsp client name
403    pub lsp_client_name: String,
404    /// Call hierarchy item to get incoming calls for
405    #[serde(deserialize_with = "string_or_struct")]
406    pub item: CallHierarchyItem,
407}
408
409/// Call hierarchy outgoing calls parameters
410#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
411pub struct CallHierarchyOutgoingCallsParams {
412    /// Unique identifier for the target Neovim instance
413    pub connection_id: String,
414    /// Lsp client name
415    pub lsp_client_name: String,
416    /// Call hierarchy item to get outgoing calls for
417    #[serde(deserialize_with = "string_or_struct")]
418    pub item: CallHierarchyItem,
419}
420
421/// Type Hierarchy Prepare parameters
422#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
423pub struct TypeHierarchyPrepareParams {
424    /// Unique identifier for the target Neovim instance
425    pub connection_id: String,
426    /// Universal document identifier
427    // Supports both string and struct deserialization.
428    // Compatible with Claude Code when using subscription.
429    #[serde(deserialize_with = "string_or_struct")]
430    pub document: DocumentIdentifier,
431    /// Lsp client name
432    pub lsp_client_name: String,
433    /// Symbol position (zero-based)
434    #[serde(flatten)]
435    pub position: Position,
436}
437
438/// Type hierarchy supertypes parameters
439#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
440pub struct TypeHierarchySupertypesParams {
441    /// Unique identifier for the target Neovim instance
442    pub connection_id: String,
443    /// Lsp client name
444    pub lsp_client_name: String,
445    /// Type hierarchy item to get supertypes for
446    #[serde(deserialize_with = "string_or_struct")]
447    pub item: TypeHierarchyItem,
448}
449
450/// Type hierarchy subtypes parameters
451#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
452pub struct TypeHierarchySubtypesParams {
453    /// Unique identifier for the target Neovim instance
454    pub connection_id: String,
455    /// Lsp client name
456    pub lsp_client_name: String,
457    /// Type hierarchy item to get subtypes for
458    #[serde(deserialize_with = "string_or_struct")]
459    pub item: TypeHierarchyItem,
460}
461
462macro_rules! include_files {
463    ($($key:ident),* $(,)?) => {{
464        let mut map = HashMap::new();
465        $(
466            map.insert(stringify!($key), include_str!(concat!("../../docs/tools/", stringify!($key), ".md")));
467        )*
468        map
469    }};
470}
471
472impl NeovimMcpServer {
473    pub fn tool_descriptions() -> HashMap<&'static str, &'static str> {
474        include_files! {
475            get_targets,
476            connect,
477            read,
478            buffer_diagnostics,
479        }
480    }
481
482    pub fn get_tool_extra_description(&self, name: &str) -> Option<String> {
483        if name == "get_targets" {
484            Some(self.get_connections_instruction())
485        } else {
486            None
487        }
488    }
489}
490
491#[tool_router]
492impl NeovimMcpServer {
493    #[tool]
494    #[instrument(skip(self))]
495    pub async fn get_targets(&self) -> Result<CallToolResult, McpError> {
496        let targets = find_get_all_targets();
497        if targets.is_empty() {
498            return Err(McpError::invalid_request(
499                "No Neovim targets found".to_string(),
500                None,
501            ));
502        }
503
504        Ok(CallToolResult::success(vec![Content::json(targets)?]))
505    }
506
507    #[tool]
508    #[instrument(skip(self))]
509    pub async fn connect(
510        &self,
511        Parameters(ConnectNvimRequest { target: path }): Parameters<ConnectNvimRequest>,
512        ctx: RequestContext<RoleServer>,
513    ) -> Result<CallToolResult, McpError> {
514        let connection_id = self.generate_shorter_connection_id(&path);
515
516        // If connection already exists, disconnect the old one first (ignoring errors)
517        if let Some(mut old_client) = self.nvim_clients.get_mut(&connection_id) {
518            let _ = old_client.disconnect().await;
519        }
520
521        let mut client = NeovimClient::default();
522        client.connect_path(&path).await?;
523
524        self.setup_new_client(&connection_id, Box::new(client), &ctx)
525            .await?;
526
527        Ok(CallToolResult::success(vec![Content::json(
528            serde_json::json!({
529                "connection_id": connection_id,
530            }),
531        )?]))
532    }
533
534    #[tool(description = "Connect via TCP address")]
535    #[instrument(skip(self))]
536    pub async fn connect_tcp(
537        &self,
538        Parameters(ConnectNvimRequest { target: address }): Parameters<ConnectNvimRequest>,
539        ctx: RequestContext<RoleServer>,
540    ) -> Result<CallToolResult, McpError> {
541        let connection_id = self.generate_shorter_connection_id(&address);
542
543        // If connection already exists, disconnect the old one first (ignoring errors)
544        if let Some(mut old_client) = self.nvim_clients.get_mut(&connection_id) {
545            let _ = old_client.disconnect().await;
546        }
547
548        let mut client = NeovimClient::default();
549        client.connect_tcp(&address).await?;
550
551        self.setup_new_client(&connection_id, Box::new(client), &ctx)
552            .await?;
553
554        Ok(CallToolResult::success(vec![Content::json(
555            serde_json::json!({
556                "connection_id": connection_id,
557            }),
558        )?]))
559    }
560
561    #[tool(description = "Disconnect from Neovim instance")]
562    #[instrument(skip(self))]
563    pub async fn disconnect(
564        &self,
565        Parameters(ConnectionRequest { connection_id }): Parameters<ConnectionRequest>,
566    ) -> Result<CallToolResult, McpError> {
567        // Verify connection exists first
568        let target = {
569            let client = self.get_connection(&connection_id)?;
570            client.target().unwrap_or_else(|| "Unknown".to_string())
571        };
572
573        // Remove the connection from the map
574        if let Some((_, mut client)) = self.nvim_clients.remove(&connection_id) {
575            if let Err(e) = client.disconnect().await {
576                return Err(McpError::internal_error(
577                    format!("Failed to disconnect: {e}"),
578                    None,
579                ));
580            }
581            Ok(CallToolResult::success(vec![Content::json(
582                serde_json::json!({
583                    "connection_id": connection_id,
584                    "target": target,
585                }),
586            )?]))
587        } else {
588            Err(McpError::invalid_request(
589                format!("No Neovim connection found for ID: {connection_id}"),
590                None,
591            ))
592        }
593    }
594
595    #[tool(description = "List all open buffers")]
596    #[instrument(skip(self))]
597    pub async fn list_buffers(
598        &self,
599        Parameters(ConnectionRequest { connection_id }): Parameters<ConnectionRequest>,
600    ) -> Result<CallToolResult, McpError> {
601        let client = self.get_connection(&connection_id)?;
602        let buffers = client.get_buffers().await?;
603        Ok(CallToolResult::success(vec![Content::json(buffers)?]))
604    }
605
606    #[tool(description = "Execute Lua code")]
607    #[instrument(skip(self))]
608    pub async fn exec_lua(
609        &self,
610        Parameters(ExecuteLuaRequest {
611            connection_id,
612            code,
613        }): Parameters<ExecuteLuaRequest>,
614    ) -> Result<CallToolResult, McpError> {
615        let client = self.get_connection(&connection_id)?;
616        let result = client.execute_lua(&code).await?;
617        Ok(CallToolResult::success(vec![Content::json(
618            serde_json::json!({
619                "result": format!("{:?}", result)
620            }),
621        )?]))
622    }
623
624    #[tool(description = "Wait for LSP client to be ready and attached")]
625    #[instrument(skip(self))]
626    pub async fn wait_for_lsp_ready(
627        &self,
628        Parameters(WaitForLspReadyRequest {
629            connection_id,
630            client_name,
631            timeout_ms,
632        }): Parameters<WaitForLspReadyRequest>,
633    ) -> Result<CallToolResult, McpError> {
634        let client = self.get_connection(&connection_id)?;
635        client
636            .wait_for_lsp_ready(client_name.as_deref(), timeout_ms)
637            .await?;
638        Ok(CallToolResult::success(vec![Content::json(
639            serde_json::json!({
640                "message": "LSP client ready",
641                "client_name": client_name.unwrap_or_else(|| "any".to_string()),
642                "timeout_ms": timeout_ms
643            }),
644        )?]))
645    }
646
647    #[tool]
648    #[instrument(skip(self))]
649    pub async fn read(
650        &self,
651        Parameters(BufferReadRequest {
652            connection_id,
653            document,
654            start,
655            end,
656        }): Parameters<BufferReadRequest>,
657    ) -> Result<CallToolResult, McpError> {
658        let client = self.get_connection(&connection_id)?;
659        let text_content = client.read_document(document, start, end).await?;
660        Ok(CallToolResult::success(vec![Content::text(text_content)]))
661    }
662
663    #[tool]
664    #[instrument(skip(self))]
665    pub async fn buffer_diagnostics(
666        &self,
667        Parameters(BufferRequest { connection_id, id }): Parameters<BufferRequest>,
668    ) -> Result<CallToolResult, McpError> {
669        let client = self.get_connection(&connection_id)?;
670        let diagnostics = client.get_buffer_diagnostics(id).await?;
671        Ok(CallToolResult::success(vec![Content::json(diagnostics)?]))
672    }
673
674    #[tool(description = "Get workspace LSP clients")]
675    #[instrument(skip(self))]
676    pub async fn lsp_clients(
677        &self,
678        Parameters(ConnectionRequest { connection_id }): Parameters<ConnectionRequest>,
679    ) -> Result<CallToolResult, McpError> {
680        let client = self.get_connection(&connection_id)?;
681        let lsp_clients = client.lsp_get_clients().await?;
682        Ok(CallToolResult::success(vec![Content::json(lsp_clients)?]))
683    }
684
685    #[tool(description = "Search workspace symbols by query")]
686    #[instrument(skip(self))]
687    pub async fn lsp_workspace_symbols(
688        &self,
689        Parameters(WorkspaceSymbolsParams {
690            connection_id,
691            lsp_client_name,
692            query,
693        }): Parameters<WorkspaceSymbolsParams>,
694    ) -> Result<CallToolResult, McpError> {
695        let client = self.get_connection(&connection_id)?;
696        let symbols = client
697            .lsp_workspace_symbols(&lsp_client_name, &query)
698            .await?;
699        Ok(CallToolResult::success(vec![Content::json(symbols)?]))
700    }
701
702    #[tool(description = "Get LSP code actions")]
703    #[instrument(skip(self))]
704    pub async fn lsp_code_actions(
705        &self,
706        Parameters(CodeActionsParams {
707            connection_id,
708            document,
709            lsp_client_name,
710            start_line,
711            start_character,
712            end_line,
713            end_character,
714        }): Parameters<CodeActionsParams>,
715    ) -> Result<CallToolResult, McpError> {
716        let client = self.get_connection(&connection_id)?;
717        let start = Position {
718            line: start_line,
719            character: start_character,
720        };
721        let end = Position {
722            line: end_line,
723            character: end_character,
724        };
725        let range = Range { start, end };
726
727        let code_actions = client
728            .lsp_get_code_actions(&lsp_client_name, document, range)
729            .await?;
730        Ok(CallToolResult::success(vec![Content::json(code_actions)?]))
731    }
732
733    #[tool(description = "Get LSP hover information")]
734    #[instrument(skip(self))]
735    pub async fn lsp_hover(
736        &self,
737        Parameters(HoverParam {
738            connection_id,
739            document,
740            lsp_client_name,
741            position,
742        }): Parameters<HoverParam>,
743    ) -> Result<CallToolResult, McpError> {
744        let client = self.get_connection(&connection_id)?;
745        let hover = client
746            .lsp_hover(&lsp_client_name, document, position)
747            .await?;
748        Ok(CallToolResult::success(vec![Content::json(hover)?]))
749    }
750
751    #[tool(description = "Get document symbols")]
752    #[instrument(skip(self))]
753    pub async fn lsp_document_symbols(
754        &self,
755        Parameters(DocumentSymbolsParams {
756            connection_id,
757            document,
758            lsp_client_name,
759        }): Parameters<DocumentSymbolsParams>,
760    ) -> Result<CallToolResult, McpError> {
761        let client = self.get_connection(&connection_id)?;
762        let symbols = client
763            .lsp_document_symbols(&lsp_client_name, document)
764            .await?;
765        Ok(CallToolResult::success(vec![Content::json(symbols)?]))
766    }
767
768    #[tool(description = "Get LSP references")]
769    #[instrument(skip(self))]
770    pub async fn lsp_references(
771        &self,
772        Parameters(ReferencesParams {
773            connection_id,
774            document,
775            lsp_client_name,
776            position,
777            include_declaration,
778        }): Parameters<ReferencesParams>,
779    ) -> Result<CallToolResult, McpError> {
780        let client = self.get_connection(&connection_id)?;
781        let references = client
782            .lsp_references(&lsp_client_name, document, position, include_declaration)
783            .await?;
784        Ok(CallToolResult::success(vec![Content::json(references)?]))
785    }
786
787    #[tool(description = "Get LSP definition")]
788    #[instrument(skip(self))]
789    pub async fn lsp_definition(
790        &self,
791        Parameters(DefinitionParams {
792            connection_id,
793            document,
794            lsp_client_name,
795            position,
796        }): Parameters<DefinitionParams>,
797    ) -> Result<CallToolResult, McpError> {
798        let client = self.get_connection(&connection_id)?;
799        let definition = client
800            .lsp_definition(&lsp_client_name, document, position)
801            .await?;
802        Ok(CallToolResult::success(vec![Content::json(definition)?]))
803    }
804
805    #[tool(description = "Get LSP type definition")]
806    #[instrument(skip(self))]
807    pub async fn lsp_type_definition(
808        &self,
809        Parameters(TypeDefinitionParams {
810            connection_id,
811            document,
812            lsp_client_name,
813            position,
814        }): Parameters<TypeDefinitionParams>,
815    ) -> Result<CallToolResult, McpError> {
816        let client = self.get_connection(&connection_id)?;
817        let type_definition = client
818            .lsp_type_definition(&lsp_client_name, document, position)
819            .await?;
820        Ok(CallToolResult::success(vec![Content::json(
821            type_definition,
822        )?]))
823    }
824
825    #[tool(description = "Get LSP implementation")]
826    #[instrument(skip(self))]
827    pub async fn lsp_implementations(
828        &self,
829        Parameters(ImplementationParams {
830            connection_id,
831            document,
832            lsp_client_name,
833            position,
834        }): Parameters<ImplementationParams>,
835    ) -> Result<CallToolResult, McpError> {
836        let client = self.get_connection(&connection_id)?;
837        let implementation = client
838            .lsp_implementation(&lsp_client_name, document, position)
839            .await?;
840        Ok(CallToolResult::success(vec![Content::json(
841            implementation,
842        )?]))
843    }
844
845    #[tool(description = "Get LSP declaration")]
846    #[instrument(skip(self))]
847    pub async fn lsp_declaration(
848        &self,
849        Parameters(DeclarationParams {
850            connection_id,
851            document,
852            lsp_client_name,
853            position,
854        }): Parameters<DeclarationParams>,
855    ) -> Result<CallToolResult, McpError> {
856        let client = self.get_connection(&connection_id)?;
857        let declaration = client
858            .lsp_declaration(&lsp_client_name, document, position)
859            .await?;
860        Ok(CallToolResult::success(vec![Content::json(declaration)?]))
861    }
862
863    #[tool(description = "Resolve a code action that may have incomplete data")]
864    #[instrument(skip(self))]
865    pub async fn lsp_resolve_code_action(
866        &self,
867        Parameters(ResolveCodeActionParams {
868            connection_id,
869            lsp_client_name,
870            code_action,
871        }): Parameters<ResolveCodeActionParams>,
872    ) -> Result<CallToolResult, McpError> {
873        let client = self.get_connection(&connection_id)?;
874        let resolved_action = client
875            .lsp_resolve_code_action(&lsp_client_name, code_action)
876            .await?;
877        Ok(CallToolResult::success(vec![Content::json(
878            resolved_action,
879        )?]))
880    }
881
882    #[tool(description = "Apply workspace edits using Neovim's LSP utility functions")]
883    #[instrument(skip(self))]
884    pub async fn lsp_apply_edit(
885        &self,
886        Parameters(ApplyWorkspaceEditParams {
887            connection_id,
888            lsp_client_name,
889            workspace_edit,
890        }): Parameters<ApplyWorkspaceEditParams>,
891    ) -> Result<CallToolResult, McpError> {
892        let client = self.get_connection(&connection_id)?;
893        client
894            .lsp_apply_workspace_edit(&lsp_client_name, workspace_edit)
895            .await?;
896        Ok(CallToolResult::success(vec![Content::text("success")]))
897    }
898
899    #[tool(description = "Rename symbol across workspace using LSP with optional validation")]
900    #[instrument(skip(self))]
901    pub async fn lsp_rename(
902        &self,
903        Parameters(RenameParams {
904            connection_id,
905            document,
906            lsp_client_name,
907            position,
908            new_name,
909            prepare_first,
910        }): Parameters<RenameParams>,
911    ) -> Result<CallToolResult, McpError> {
912        let client = self.get_connection(&connection_id)?;
913
914        // Optionally run prepare rename first to validate the position
915        if prepare_first {
916            match client
917                .lsp_prepare_rename(&lsp_client_name, document.clone(), position.clone())
918                .await
919            {
920                Ok(Some(prepare_result)) => {
921                    // Prepare rename was successful, we can proceed
922                    let prepare_info = match prepare_result {
923                        PrepareRenameResult::Range(range) => {
924                            format!("Range: {:?}", range)
925                        }
926                        PrepareRenameResult::RangeWithPlaceholder { range, placeholder } => {
927                            format!("Range: {:?}, Current name: '{}'", range, placeholder)
928                        }
929                        PrepareRenameResult::DefaultBehavior { .. } => {
930                            "Default behavior enabled".to_string()
931                        }
932                    };
933                    tracing::debug!("Prepare rename successful: {}", prepare_info);
934                }
935                Ok(None) => {
936                    return Err(McpError::invalid_request(
937                        "Position is not renameable according to prepare rename".to_string(),
938                        None,
939                    ));
940                }
941                Err(e) => {
942                    return Err(McpError::invalid_request(
943                        format!("Prepare rename failed: {}", e),
944                        None,
945                    ));
946                }
947            }
948        }
949
950        // Proceed with the actual rename
951        let workspace_edit = client
952            .lsp_rename(&lsp_client_name, document, position, &new_name)
953            .await?;
954
955        if let Some(edit) = workspace_edit {
956            // Apply the workspace edit automatically
957            client
958                .lsp_apply_workspace_edit(&lsp_client_name, edit)
959                .await?;
960            Ok(CallToolResult::success(vec![Content::text(
961                "Rename completed successfully",
962            )]))
963        } else {
964            Err(McpError::invalid_request(
965                "Rename operation is not valid at this position".to_string(),
966                None,
967            ))
968        }
969    }
970
971    #[tool(description = "Format entire document using LSP with optional auto-apply")]
972    #[instrument(skip(self))]
973    pub async fn lsp_formatting(
974        &self,
975        Parameters(DocumentFormattingParams {
976            connection_id,
977            document,
978            lsp_client_name,
979            options,
980            apply_edits,
981        }): Parameters<DocumentFormattingParams>,
982    ) -> Result<CallToolResult, McpError> {
983        let client = self.get_connection(&connection_id)?;
984        let text_edits = client
985            .lsp_formatting(&lsp_client_name, document.clone(), options)
986            .await?;
987
988        if apply_edits {
989            // Apply the text edits automatically
990            client
991                .lsp_apply_text_edits(&lsp_client_name, document, text_edits)
992                .await?;
993            Ok(CallToolResult::success(vec![Content::text(
994                "Formatting applied successfully",
995            )]))
996        } else {
997            // Return the text edits for inspection
998            Ok(CallToolResult::success(vec![Content::json(text_edits)?]))
999        }
1000    }
1001
1002    #[tool(
1003        description = "Format a specific range in a document using LSP with optional auto-apply"
1004    )]
1005    #[instrument(skip(self))]
1006    pub async fn lsp_range_formatting(
1007        &self,
1008        Parameters(DocumentRangeFormattingParams {
1009            connection_id,
1010            document,
1011            lsp_client_name,
1012            start_line,
1013            start_character,
1014            end_line,
1015            end_character,
1016            options,
1017            apply_edits,
1018        }): Parameters<DocumentRangeFormattingParams>,
1019    ) -> Result<CallToolResult, McpError> {
1020        let client = self.get_connection(&connection_id)?;
1021        let start = Position {
1022            line: start_line,
1023            character: start_character,
1024        };
1025        let end = Position {
1026            line: end_line,
1027            character: end_character,
1028        };
1029        let range = Range { start, end };
1030
1031        let text_edits = client
1032            .lsp_range_formatting(&lsp_client_name, document.clone(), range, options)
1033            .await?;
1034
1035        if apply_edits {
1036            // Apply the text edits automatically
1037            client
1038                .lsp_apply_text_edits(&lsp_client_name, document, text_edits)
1039                .await?;
1040            Ok(CallToolResult::success(vec![Content::text(
1041                "Range formatting applied successfully",
1042            )]))
1043        } else {
1044            // Return the text edits for inspection
1045            Ok(CallToolResult::success(vec![Content::json(text_edits)?]))
1046        }
1047    }
1048
1049    #[tool(description = "Sort and organize imports")]
1050    #[instrument(skip(self))]
1051    pub async fn lsp_organize_imports(
1052        &self,
1053        Parameters(LspOrganizeImportsParams {
1054            connection_id,
1055            document,
1056            lsp_client_name,
1057            apply_edits,
1058        }): Parameters<LspOrganizeImportsParams>,
1059    ) -> Result<CallToolResult, McpError> {
1060        let client = self.get_connection(&connection_id)?;
1061
1062        // Get organize imports code actions for the entire document
1063        let code_actions = client
1064            .lsp_get_organize_imports_actions(&lsp_client_name, document)
1065            .await?;
1066
1067        if code_actions.is_empty() {
1068            return Ok(CallToolResult::success(vec![Content::text(
1069                "No organize imports actions available for this document",
1070            )]));
1071        }
1072
1073        if !apply_edits {
1074            // Return the code actions for inspection
1075            return Ok(CallToolResult::success(vec![Content::json(code_actions)?]));
1076        }
1077
1078        // Apply the first/preferred organize imports action
1079        let action = code_actions[0].clone();
1080
1081        // Resolve the action if it needs resolution
1082        let resolved_action = if action.has_edit() {
1083            action
1084        } else {
1085            client
1086                .lsp_resolve_code_action(&lsp_client_name, action)
1087                .await?
1088        };
1089
1090        // Apply the workspace edit
1091        if let Some(edit) = resolved_action.edit() {
1092            client
1093                .lsp_apply_workspace_edit(&lsp_client_name, edit.clone())
1094                .await?;
1095            Ok(CallToolResult::success(vec![Content::text(
1096                "Imports organized successfully",
1097            )]))
1098        } else {
1099            Err(McpError::invalid_request(
1100                "Organize imports action does not contain workspace edit".to_string(),
1101                None,
1102            ))
1103        }
1104    }
1105
1106    #[tool(
1107        description = "Get the current cursor position: buffer id, buffer name, window id, and zero-based row/col index"
1108    )]
1109    #[instrument(skip(self))]
1110    pub async fn cursor_position(
1111        &self,
1112        Parameters(ConnectionRequest { connection_id }): Parameters<ConnectionRequest>,
1113    ) -> Result<CallToolResult, McpError> {
1114        let client = self.get_connection(&connection_id)?;
1115        let lua_code = include_str!("./lua/cursor_position.lua");
1116        let result = client.execute_lua(lua_code).await?;
1117
1118        // Convert nvim Value to serde_json::Value for serialization
1119        let json_result = lua_tools::convert_nvim_value_to_json(result).map_err(|e| {
1120            McpError::internal_error(
1121                format!("Failed to convert cursor position result to JSON: {}", e),
1122                None,
1123            )
1124        })?;
1125
1126        Ok(CallToolResult::success(vec![Content::json(json_result)?]))
1127    }
1128
1129    #[tool(
1130        description = "Navigate to a specific position in the current buffer or open a file at a specific position"
1131    )]
1132    #[instrument(skip(self))]
1133    pub async fn navigate(
1134        &self,
1135        Parameters(NavigateParams {
1136            connection_id,
1137            document,
1138            position,
1139        }): Parameters<NavigateParams>,
1140    ) -> Result<CallToolResult, McpError> {
1141        let client = self.get_connection(&connection_id)?;
1142        let result = client.navigate(document, position).await?;
1143        Ok(CallToolResult::success(vec![Content::json(result)?]))
1144    }
1145
1146    #[tool(description = "Prepare call hierarchy for a symbol at a specific position")]
1147    #[instrument(skip(self))]
1148    pub async fn lsp_call_hierarchy_prepare(
1149        &self,
1150        Parameters(CallHierarchyPrepareParams {
1151            connection_id,
1152            document,
1153            lsp_client_name,
1154            position,
1155        }): Parameters<CallHierarchyPrepareParams>,
1156    ) -> Result<CallToolResult, McpError> {
1157        let client = self.get_connection(&connection_id)?;
1158        let result = client
1159            .lsp_call_hierarchy_prepare(&lsp_client_name, document, position)
1160            .await?;
1161        Ok(CallToolResult::success(vec![Content::json(result)?]))
1162    }
1163
1164    #[tool(description = "Get incoming calls for a call hierarchy item")]
1165    #[instrument(skip(self))]
1166    pub async fn lsp_call_hierarchy_incoming_calls(
1167        &self,
1168        Parameters(CallHierarchyIncomingCallsParams {
1169            connection_id,
1170            lsp_client_name,
1171            item,
1172        }): Parameters<CallHierarchyIncomingCallsParams>,
1173    ) -> Result<CallToolResult, McpError> {
1174        let client = self.get_connection(&connection_id)?;
1175        let result = client
1176            .lsp_call_hierarchy_incoming_calls(&lsp_client_name, item)
1177            .await?;
1178        Ok(CallToolResult::success(vec![Content::json(result)?]))
1179    }
1180
1181    #[tool(description = "Get outgoing calls for a call hierarchy item")]
1182    #[instrument(skip(self))]
1183    pub async fn lsp_call_hierarchy_outgoing_calls(
1184        &self,
1185        Parameters(CallHierarchyOutgoingCallsParams {
1186            connection_id,
1187            lsp_client_name,
1188            item,
1189        }): Parameters<CallHierarchyOutgoingCallsParams>,
1190    ) -> Result<CallToolResult, McpError> {
1191        let client = self.get_connection(&connection_id)?;
1192        let result = client
1193            .lsp_call_hierarchy_outgoing_calls(&lsp_client_name, item)
1194            .await?;
1195        Ok(CallToolResult::success(vec![Content::json(result)?]))
1196    }
1197
1198    #[tool(description = "Prepare type hierarchy for a symbol at a specific position")]
1199    #[instrument(skip(self))]
1200    pub async fn lsp_type_hierarchy_prepare(
1201        &self,
1202        Parameters(TypeHierarchyPrepareParams {
1203            connection_id,
1204            document,
1205            lsp_client_name,
1206            position,
1207        }): Parameters<TypeHierarchyPrepareParams>,
1208    ) -> Result<CallToolResult, McpError> {
1209        let client = self.get_connection(&connection_id)?;
1210        let result = client
1211            .lsp_type_hierarchy_prepare(&lsp_client_name, document, position)
1212            .await?;
1213        Ok(CallToolResult::success(vec![Content::json(result)?]))
1214    }
1215
1216    #[tool(description = "Get supertypes for a type hierarchy item")]
1217    #[instrument(skip(self))]
1218    pub async fn lsp_type_hierarchy_supertypes(
1219        &self,
1220        Parameters(TypeHierarchySupertypesParams {
1221            connection_id,
1222            lsp_client_name,
1223            item,
1224        }): Parameters<TypeHierarchySupertypesParams>,
1225    ) -> Result<CallToolResult, McpError> {
1226        let client = self.get_connection(&connection_id)?;
1227        let result = client
1228            .lsp_type_hierarchy_supertypes(&lsp_client_name, item)
1229            .await?;
1230        Ok(CallToolResult::success(vec![Content::json(result)?]))
1231    }
1232
1233    #[tool(description = "Get subtypes for a type hierarchy item")]
1234    #[instrument(skip(self))]
1235    pub async fn lsp_type_hierarchy_subtypes(
1236        &self,
1237        Parameters(TypeHierarchySubtypesParams {
1238            connection_id,
1239            lsp_client_name,
1240            item,
1241        }): Parameters<TypeHierarchySubtypesParams>,
1242    ) -> Result<CallToolResult, McpError> {
1243        let client = self.get_connection(&connection_id)?;
1244        let result = client
1245            .lsp_type_hierarchy_subtypes(&lsp_client_name, item)
1246            .await?;
1247        Ok(CallToolResult::success(vec![Content::json(result)?]))
1248    }
1249}
1250
1251/// Build tool router for NeovimMcpServer
1252pub fn build_tool_router() -> ToolRouter<NeovimMcpServer> {
1253    NeovimMcpServer::tool_router()
1254}