#
tokens: 18299/50000 6/6 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── src
│   ├── index.ts
│   └── neovim.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # Dependencies
 2 | node_modules/
 3 | 
 4 | # Build output
 5 | build/
 6 | dist/
 7 | 
 8 | # Environment variables
 9 | .env
10 | 
11 | # IDE and OS files
12 | .DS_Store
13 | .vscode/
14 | .idea/
15 | 
16 | # Logs
17 | *.log
18 | npm-debug.log*
19 | yarn-debug.log*
20 | yarn-error.log*
21 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Neovim MCP Server
  2 | 
  3 | Connect Claude Desktop (or any Model Context Protocol client) to Neovim using MCP and the official neovim/node-client JavaScript library. This server leverages Vim's native text editing commands and workflows, which Claude already understands, to create a lightweight code or general purpose AI text assistance layer.
  4 | 
  5 | <a href="https://glama.ai/mcp/servers/s0fywdwp87"><img width="380" height="200" src="https://glama.ai/mcp/servers/s0fywdwp87/badge" alt="mcp-neovim-server MCP server" /></a>
  6 | 
  7 | ## Features
  8 | 
  9 | - Connects to your nvim instance if you expose a socket file, for example `--listen /tmp/nvim`, when starting nvim
 10 | - Views your current buffers and manages buffer switching
 11 | - Gets cursor location, mode, file name, marks, registers, and visual selections
 12 | - Runs vim commands and optionally shell commands through vim
 13 | - Can make edits using insert, replace, or replaceAll modes
 14 | - Search and replace functionality with regex support
 15 | - Project-wide grep search with quickfix integration
 16 | - Comprehensive window management
 17 | - Health monitoring and connection diagnostics
 18 | 
 19 | ## API
 20 | 
 21 | ### Resources
 22 | 
 23 | - `nvim://session`: Current neovim text editor session
 24 | - `nvim://buffers`: List of all open buffers in the current Neovim session with metadata including modified status, syntax, and window IDs
 25 | 
 26 | ### Tools
 27 | 
 28 | #### Core Tools
 29 | - **vim_buffer**
 30 |   - Get buffer contents with line numbers (supports filename parameter)
 31 |   - Input `filename` (string, optional) - Get specific buffer by filename
 32 |   - Returns numbered lines with buffer content
 33 | - **vim_command**
 34 |   - Send a command to VIM for navigation, spot editing, and line deletion
 35 |   - Input `command` (string)
 36 |   - Runs vim commands with `nvim.replaceTermcodes`. Multiple commands work with newlines
 37 |   - Shell commands supported with `!` prefix when `ALLOW_SHELL_COMMANDS=true`
 38 |   - On error, `'nvim:errmsg'` contents are returned 
 39 | - **vim_status**
 40 |   - Get comprehensive Neovim status
 41 |   - Returns cursor position, mode, filename, visual selection with enhanced detection, window layout, current tab, marks, registers, working directory, LSP client info, and plugin detection
 42 |   - Enhanced visual selection reporting: detects visual mode type (character/line/block), provides accurate selection text, start/end positions, and last visual selection marks
 43 | - **vim_edit**
 44 |   - Edit lines using insert, replace, or replaceAll modes
 45 |   - Input `startLine` (number), `mode` (`"insert"` | `"replace"` | `"replaceAll"`), `lines` (string)
 46 |   - insert: insert lines at startLine
 47 |   - replace: replace lines starting at startLine
 48 |   - replaceAll: replace entire buffer contents
 49 | - **vim_window**
 50 |   - Manipulate Neovim windows (split, vsplit, close, navigate)
 51 |   - Input `command` (string: "split", "vsplit", "only", "close", "wincmd h/j/k/l")
 52 | - **vim_mark**
 53 |   - Set named marks at specific positions
 54 |   - Input `mark` (string: a-z), `line` (number), `column` (number)
 55 | - **vim_register**
 56 |   - Set content of registers
 57 |   - Input `register` (string: a-z or "), `content` (string)
 58 | - **vim_visual**
 59 |   - Create visual mode selections
 60 |   - Input `startLine` (number), `startColumn` (number), `endLine` (number), `endColumn` (number)
 61 | 
 62 | #### Enhanced Buffer Management
 63 | - **vim_buffer_switch**
 64 |   - Switch between buffers by name or number
 65 |   - Input `identifier` (string | number) - Buffer name or number
 66 | - **vim_buffer_save**
 67 |   - Save current buffer or save to specific filename
 68 |   - Input `filename` (string, optional) - Save to specific file
 69 | - **vim_file_open**
 70 |   - Open files into new buffers
 71 |   - Input `filename` (string) - File to open
 72 | 
 73 | #### Search and Replace
 74 | - **vim_search**
 75 |   - Search within current buffer with regex support
 76 |   - Input `pattern` (string), `ignoreCase` (boolean, optional), `wholeWord` (boolean, optional)
 77 | - **vim_search_replace**
 78 |   - Find and replace with advanced options
 79 |   - Input `pattern` (string), `replacement` (string), `global` (boolean, optional), `ignoreCase` (boolean, optional), `confirm` (boolean, optional)
 80 | - **vim_grep**
 81 |   - Project-wide search using vimgrep with quickfix list
 82 |   - Input `pattern` (string), `filePattern` (string, optional) - File pattern to search
 83 | 
 84 | #### Advanced Workflow Tools
 85 | - **vim_macro**
 86 |   - Record, stop, and play Vim macros
 87 |   - Input `action` ("record" | "stop" | "play"), `register` (string, a-z), `count` (number, optional)
 88 | - **vim_tab**
 89 |   - Complete tab management
 90 |   - Input `action` ("new" | "close" | "next" | "prev" | "first" | "last" | "list"), `filename` (string, optional)
 91 | - **vim_fold**
 92 |   - Code folding operations
 93 |   - Input `action` ("create" | "open" | "close" | "toggle" | "openall" | "closeall" | "delete"), `startLine`/`endLine` (numbers, for create)
 94 | - **vim_jump**
 95 |   - Jump list navigation
 96 |   - Input `direction` ("back" | "forward" | "list")
 97 | 
 98 | #### System Tools
 99 | - **vim_health**
100 |   - Check Neovim connection health and socket status
101 | 
102 | Using this comprehensive set of **19 tools**, Claude can peer into your neovim session, navigate buffers, perform searches, make edits, record macros, manage tabs and folds, and handle your complete development workflow with standard Neovim features.
103 | 
104 | ### Prompts
105 | 
106 | - **neovim_workflow**: Get contextual help and guidance for common Neovim workflows including editing, navigation, search, buffer management, window operations, and macro usage. Provides step-by-step instructions for accomplishing tasks with the available MCP tools.
107 | 
108 | ## Error Handling
109 | 
110 | The server implements comprehensive error handling with custom error classes and consistent error responses:
111 | 
112 | - **NeovimConnectionError**: Socket connection failures with detailed messages
113 | - **NeovimCommandError**: Command execution failures with command context  
114 | - **NeovimValidationError**: Input validation failures
115 | 
116 | **New in v0.5.2**: All tools now include robust try-catch error handling that returns meaningful error messages in proper MCP format. Features include connection health monitoring, graceful error propagation, and actionable error messages to help diagnose issues.
117 | 
118 | ## Limitations
119 | 
120 | - May not interact well with complex neovim configurations or plugins
121 | - Shell command execution is disabled by default for security
122 | - Socket connection required - won't work with standard vim
123 | 
124 | ## Configuration
125 | 
126 | ### Environment Variables
127 | 
128 | - `ALLOW_SHELL_COMMANDS`: Set to 'true' to enable shell command execution (e.g. `!ls`). Defaults to false for security.
129 | - `NVIM_SOCKET_PATH`: Set to the path of your Neovim socket. Defaults to '/tmp/nvim' if not specified.
130 | 
131 | ## Installation
132 | 
133 | ### Option 1: DXT Package (Recommended)
134 | 1. Download the latest `.dxt` file from [Releases](https://github.com/bigcodegen/mcp-neovim-server/releases)
135 | 2. Drag the file to Claude Desktop
136 | 
137 | ### Option 2: Manual Installation
138 | Add this to your `claude_desktop_config.json`:
139 | ```json
140 | {
141 |   "mcpServers": {
142 |     "MCP Neovim Server": {
143 |       "command": "npx",
144 |       "args": [
145 |         "-y",
146 |         "mcp-neovim-server"
147 |       ],
148 |       "env": {
149 |         "ALLOW_SHELL_COMMANDS": "true",
150 |         "NVIM_SOCKET_PATH": "/tmp/nvim"
151 |       }
152 |     }
153 |   }
154 | }
155 | ```
156 | 
157 | ## License
158 | 
159 | This MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
160 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "Node16",
 5 |     "moduleResolution": "Node16",
 6 |     "outDir": "./build",
 7 |     "rootDir": "./src",
 8 |     "strict": true,
 9 |     "esModuleInterop": true,
10 |     "skipLibCheck": true,
11 |     "forceConsistentCasingInFileNames": true
12 |   },
13 |   "include": ["src/**/*"],
14 |   "exclude": ["node_modules"]
15 | }
16 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "mcp-neovim-server",
 3 |   "version": "0.5.5",
 4 |   "description": "An MCP server for neovim",
 5 |   "type": "module",
 6 |   "bin": {
 7 |     "mcp-neovim-server": "build/index.js"
 8 |   },
 9 |   "files": [
10 |     "build"
11 |   ],
12 |   "scripts": {
13 |     "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
14 |     "prepare": "npm run build",
15 |     "watch": "tsc --watch",
16 |     "inspector": "npx @modelcontextprotocol/inspector build/index.js"
17 |   },
18 |   "repository": {
19 |     "type": "git",
20 |     "url": "git+https://github.com/bigcodegen/mcp-neovim-server.git"
21 |   },
22 |   "author": "bigcodegen",
23 |   "license": "MIT",
24 |   "bugs": {
25 |     "url": "https://github.com/bigcodegen/mcp-neovim-server/issues"
26 |   },
27 |   "homepage": "https://github.com/bigcodegen/mcp-neovim-server#readme",
28 |   "dependencies": {
29 |     "@modelcontextprotocol/sdk": "^1.17.4",
30 |     "neovim": "^5.3.0",
31 |     "ts-node": "^10.9.2",
32 |     "zod": "^3.25.76"
33 |   },
34 |   "devDependencies": {
35 |     "@types/node": "^24.3.0",
36 |     "typescript": "^5.9.2"
37 |   }
38 | }
39 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | /**
  4 |  * This is an MCP server that connects to neovim.
  5 |  */
  6 | 
  7 | import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
  8 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  9 | import { NeovimManager, NeovimConnectionError, NeovimCommandError, NeovimValidationError } from "./neovim.js";
 10 | import { z } from "zod";
 11 | 
 12 | const server = new McpServer(
 13 |   {
 14 |     name: "mcp-neovim-server",
 15 |     version: "0.5.5"
 16 |   }
 17 | );
 18 | 
 19 | const neovimManager = NeovimManager.getInstance();
 20 | 
 21 | // Register resources
 22 | server.resource(
 23 |   "session",
 24 |   new ResourceTemplate("nvim://session", { 
 25 |     list: () => ({
 26 |       resources: [{
 27 |         uri: "nvim://session",
 28 |         mimeType: "text/plain",
 29 |         name: "Current neovim session",
 30 |         description: "Current neovim text editor session"
 31 |       }]
 32 |     })
 33 |   }),
 34 |   async (uri) => {
 35 |     const bufferContents = await neovimManager.getBufferContents();
 36 |     return {
 37 |       contents: [{
 38 |         uri: uri.href,
 39 |         mimeType: "text/plain",
 40 |         text: Array.from(bufferContents.entries())
 41 |           .map(([lineNum, lineText]) => `${lineNum}: ${lineText}`)
 42 |           .join('\n')
 43 |       }]
 44 |     };
 45 |   }
 46 | );
 47 | 
 48 | server.resource(
 49 |   "buffers",
 50 |   new ResourceTemplate("nvim://buffers", { 
 51 |     list: () => ({
 52 |       resources: [{
 53 |         uri: "nvim://buffers",
 54 |         mimeType: "application/json",
 55 |         name: "Open Neovim buffers",
 56 |         description: "List of all open buffers in the current Neovim session"
 57 |       }]
 58 |     })
 59 |   }),
 60 |   async (uri) => {
 61 |     const openBuffers = await neovimManager.getOpenBuffers();
 62 |     return {
 63 |       contents: [{
 64 |         uri: uri.href,
 65 |         mimeType: "application/json",
 66 |         text: JSON.stringify(openBuffers, null, 2)
 67 |       }]
 68 |     };
 69 |   }
 70 | );
 71 | 
 72 | // Register tools with proper parameter schemas
 73 | server.tool(
 74 |   "vim_buffer",
 75 |   "Get buffer contents with line numbers",
 76 |   { filename: z.string().optional().describe("Optional file name to view a specific buffer") },
 77 |   async ({ filename }) => {
 78 |     try {
 79 |       const bufferContents = await neovimManager.getBufferContents(filename);
 80 |       return {
 81 |         content: [{
 82 |           type: "text",
 83 |           text: Array.from(bufferContents.entries())
 84 |             .map(([lineNum, lineText]) => `${lineNum}: ${lineText}`)
 85 |             .join('\n')
 86 |         }]
 87 |       };
 88 |     } catch (error) {
 89 |       return {
 90 |         content: [{
 91 |           type: "text",
 92 |           text: error instanceof Error ? error.message : 'Error getting buffer contents'
 93 |         }]
 94 |       };
 95 |     }
 96 |   }
 97 | );
 98 | 
 99 | server.tool(
100 |   "vim_command",
101 |   "Execute Vim commands with optional shell command support",
102 |   { command: z.string().describe("Vim command to execute (use ! prefix for shell commands if enabled)") },
103 |   async ({ command }) => {
104 |     try {
105 |       // Check if this is a shell command
106 |       if (command.startsWith('!')) {
107 |         const allowShellCommands = process.env.ALLOW_SHELL_COMMANDS === 'true';
108 |         if (!allowShellCommands) {
109 |           return {
110 |             content: [{
111 |               type: "text",
112 |               text: "Shell command execution is disabled. Set ALLOW_SHELL_COMMANDS=true environment variable to enable shell commands."
113 |             }]
114 |           };
115 |         }
116 |       }
117 | 
118 |       const result = await neovimManager.sendCommand(command);
119 |       return {
120 |         content: [{
121 |           type: "text",
122 |           text: result
123 |         }]
124 |       };
125 |     } catch (error) {
126 |       return {
127 |         content: [{
128 |           type: "text",
129 |           text: error instanceof Error ? error.message : 'Error executing command'
130 |         }]
131 |       };
132 |     }
133 |   }
134 | );
135 | 
136 | server.tool(
137 |   "vim_status",
138 |   "Get comprehensive Neovim status including cursor position, mode, marks, and registers",
139 |   {},
140 |   async () => {
141 |     try {
142 |       const status = await neovimManager.getNeovimStatus();
143 |       return {
144 |         content: [{
145 |           type: "text",
146 |           text: JSON.stringify(status, null, 2)
147 |         }]
148 |       };
149 |     } catch (error) {
150 |       return {
151 |         content: [{
152 |           type: "text",
153 |           text: error instanceof Error ? error.message : 'Error getting Neovim status'
154 |         }]
155 |       };
156 |     }
157 |   }
158 | );
159 | 
160 | server.tool(
161 |   "vim_edit",
162 |   "Edit buffer content using insert, replace, or replaceAll modes",
163 |   { 
164 |     startLine: z.number().describe("The line number where editing should begin (1-indexed)"),
165 |     mode: z.enum(["insert", "replace", "replaceAll"]).describe("Whether to insert new content, replace existing content, or replace entire buffer"),
166 |     lines: z.string().describe("The text content to insert or use as replacement")
167 |   },
168 |   async ({ startLine, mode, lines }) => {
169 |     try {
170 |       const result = await neovimManager.editLines(startLine, mode, lines);
171 |       return {
172 |         content: [{
173 |           type: "text",
174 |           text: result
175 |         }]
176 |       };
177 |     } catch (error) {
178 |       return {
179 |         content: [{
180 |           type: "text",
181 |           text: error instanceof Error ? error.message : 'Error editing buffer'
182 |         }]
183 |       };
184 |     }
185 |   }
186 | );
187 | 
188 | server.tool(
189 |   "vim_window",
190 |   "Manage Neovim windows: split, close, and navigate between windows",
191 |   { 
192 |     command: z.enum(["split", "vsplit", "only", "close", "wincmd h", "wincmd j", "wincmd k", "wincmd l"])
193 |       .describe("Window manipulation command: split or vsplit to create new window, only to keep just current window, close to close current window, or wincmd with h/j/k/l to navigate between windows")
194 |   },
195 |   async ({ command }) => {
196 |     try {
197 |       const result = await neovimManager.manipulateWindow(command);
198 |       return {
199 |         content: [{
200 |           type: "text",
201 |           text: result
202 |         }]
203 |       };
204 |     } catch (error) {
205 |       return {
206 |         content: [{
207 |           type: "text",
208 |           text: error instanceof Error ? error.message : 'Error manipulating window'
209 |         }]
210 |       };
211 |     }
212 |   }
213 | );
214 | 
215 | server.tool(
216 |   "vim_mark",
217 |   "Set named marks at specific positions in the buffer",
218 |   {
219 |     mark: z.string().regex(/^[a-z]$/).describe("Single lowercase letter [a-z] to use as the mark name"),
220 |     line: z.number().describe("The line number where the mark should be placed (1-indexed)"),
221 |     column: z.number().describe("The column number where the mark should be placed (0-indexed)")
222 |   },
223 |   async ({ mark, line, column }) => {
224 |     try {
225 |       const result = await neovimManager.setMark(mark, line, column);
226 |       return {
227 |         content: [{
228 |           type: "text",
229 |           text: result
230 |         }]
231 |       };
232 |     } catch (error) {
233 |       return {
234 |         content: [{
235 |           type: "text",
236 |           text: error instanceof Error ? error.message : 'Error setting mark'
237 |         }]
238 |       };
239 |     }
240 |   }
241 | );
242 | 
243 | server.tool(
244 |   "vim_register",
245 |   "Manage Neovim register contents",
246 |   {
247 |     register: z.string().regex(/^[a-z"]$/).describe("Register name - a lowercase letter [a-z] or double-quote [\"] for the unnamed register"),
248 |     content: z.string().describe("The text content to store in the specified register")
249 |   },
250 |   async ({ register, content }) => {
251 |     try {
252 |       const result = await neovimManager.setRegister(register, content);
253 |       return {
254 |         content: [{
255 |           type: "text",
256 |           text: result
257 |         }]
258 |       };
259 |     } catch (error) {
260 |       return {
261 |         content: [{
262 |           type: "text",
263 |           text: error instanceof Error ? error.message : 'Error setting register'
264 |         }]
265 |       };
266 |     }
267 |   }
268 | );
269 | 
270 | server.tool(
271 |   "vim_visual",
272 |   "Create visual mode selections in the buffer",
273 |   {
274 |     startLine: z.number().describe("The starting line number for visual selection (1-indexed)"),
275 |     startColumn: z.number().describe("The starting column number for visual selection (0-indexed)"),
276 |     endLine: z.number().describe("The ending line number for visual selection (1-indexed)"),
277 |     endColumn: z.number().describe("The ending column number for visual selection (0-indexed)")
278 |   },
279 |   async ({ startLine, startColumn, endLine, endColumn }) => {
280 |     try {
281 |       const result = await neovimManager.visualSelect(startLine, startColumn, endLine, endColumn);
282 |       return {
283 |         content: [{
284 |           type: "text",
285 |           text: result
286 |         }]
287 |       };
288 |     } catch (error) {
289 |       return {
290 |         content: [{
291 |           type: "text",
292 |           text: error instanceof Error ? error.message : 'Error creating visual selection'
293 |         }]
294 |       };
295 |     }
296 |   }
297 | );
298 | 
299 | // New enhanced buffer management tools
300 | server.tool(
301 |   "vim_buffer_switch",
302 |   "Switch between buffers by name or number",
303 |   {
304 |     identifier: z.union([z.string(), z.number()]).describe("Buffer identifier - can be buffer number or filename/path")
305 |   },
306 |   async ({ identifier }) => {
307 |     try {
308 |       const result = await neovimManager.switchBuffer(identifier);
309 |       return {
310 |         content: [{
311 |           type: "text",
312 |           text: result
313 |         }]
314 |       };
315 |     } catch (error) {
316 |       return {
317 |         content: [{
318 |           type: "text",
319 |           text: error instanceof Error ? error.message : 'Error switching buffer'
320 |         }]
321 |       };
322 |     }
323 |   }
324 | );
325 | 
326 | server.tool(
327 |   "vim_buffer_save",
328 |   "Save current buffer or save to specific filename",
329 |   {
330 |     filename: z.string().optional().describe("Optional filename to save buffer to (defaults to current buffer's filename)")
331 |   },
332 |   async ({ filename }) => {
333 |     try {
334 |       const result = await neovimManager.saveBuffer(filename);
335 |       return {
336 |         content: [{
337 |           type: "text",
338 |           text: result
339 |         }]
340 |       };
341 |     } catch (error) {
342 |       return {
343 |         content: [{
344 |           type: "text",
345 |           text: error instanceof Error ? error.message : 'Error saving buffer'
346 |         }]
347 |       };
348 |     }
349 |   }
350 | );
351 | 
352 | server.tool(
353 |   "vim_file_open",
354 |   "Open files into new buffers",
355 |   {
356 |     filename: z.string().describe("Path to the file to open")
357 |   },
358 |   async ({ filename }) => {
359 |     try {
360 |       const result = await neovimManager.openFile(filename);
361 |       return {
362 |         content: [{
363 |           type: "text",
364 |           text: result
365 |         }]
366 |       };
367 |     } catch (error) {
368 |       return {
369 |         content: [{
370 |           type: "text",
371 |           text: error instanceof Error ? error.message : 'Error opening file'
372 |         }]
373 |       };
374 |     }
375 |   }
376 | );
377 | 
378 | // New search and replace tools
379 | server.tool(
380 |   "vim_search",
381 |   "Search within current buffer with regex support and options",
382 |   {
383 |     pattern: z.string().describe("Search pattern (supports regex)"),
384 |     ignoreCase: z.boolean().optional().describe("Whether to ignore case in search (default: false)"),
385 |     wholeWord: z.boolean().optional().describe("Whether to match whole words only (default: false)")
386 |   },
387 |   async ({ pattern, ignoreCase = false, wholeWord = false }) => {
388 |     try {
389 |       const result = await neovimManager.searchInBuffer(pattern, { ignoreCase, wholeWord });
390 |       return {
391 |         content: [{
392 |           type: "text",
393 |           text: result
394 |         }]
395 |       };
396 |     } catch (error) {
397 |       return {
398 |         content: [{
399 |           type: "text",
400 |           text: error instanceof Error ? error.message : 'Error searching in buffer'
401 |         }]
402 |       };
403 |     }
404 |   }
405 | );
406 | 
407 | server.tool(
408 |   "vim_search_replace",
409 |   "Find and replace with global, case-insensitive, and confirm options",
410 |   {
411 |     pattern: z.string().describe("Search pattern (supports regex)"),
412 |     replacement: z.string().describe("Replacement text"),
413 |     global: z.boolean().optional().describe("Replace all occurrences in each line (default: false)"),
414 |     ignoreCase: z.boolean().optional().describe("Whether to ignore case in search (default: false)"),
415 |     confirm: z.boolean().optional().describe("Whether to confirm each replacement (default: false)")
416 |   },
417 |   async ({ pattern, replacement, global = false, ignoreCase = false, confirm = false }) => {
418 |     try {
419 |       const result = await neovimManager.searchAndReplace(pattern, replacement, { global, ignoreCase, confirm });
420 |       return {
421 |         content: [{
422 |           type: "text",
423 |           text: result
424 |         }]
425 |       };
426 |     } catch (error) {
427 |       return {
428 |         content: [{
429 |           type: "text",
430 |           text: error instanceof Error ? error.message : 'Error in search and replace'
431 |         }]
432 |       };
433 |     }
434 |   }
435 | );
436 | 
437 | server.tool(
438 |   "vim_grep",
439 |   "Project-wide search using vimgrep with quickfix list",
440 |   {
441 |     pattern: z.string().describe("Search pattern to grep for"),
442 |     filePattern: z.string().optional().describe("File pattern to search in (default: **/* for all files)")
443 |   },
444 |   async ({ pattern, filePattern = "**/*" }) => {
445 |     try {
446 |       const result = await neovimManager.grepInProject(pattern, filePattern);
447 |       return {
448 |         content: [{
449 |           type: "text",
450 |           text: result
451 |         }]
452 |       };
453 |     } catch (error) {
454 |       return {
455 |         content: [{
456 |           type: "text",
457 |           text: error instanceof Error ? error.message : 'Error in grep search'
458 |         }]
459 |       };
460 |     }
461 |   }
462 | );
463 | 
464 | // Health check tool
465 | server.tool(
466 |   "vim_health",
467 |   "Check Neovim connection health",
468 |   {},
469 |   async () => {
470 |     const isHealthy = await neovimManager.healthCheck();
471 |     return {
472 |       content: [{
473 |         type: "text",
474 |         text: isHealthy ? "Neovim connection is healthy" : "Neovim connection failed"
475 |       }]
476 |     };
477 |   }
478 | );
479 | 
480 | // Macro management tool
481 | server.tool(
482 |   "vim_macro",
483 |   "Record, stop, and play Neovim macros",
484 |   {
485 |     action: z.enum(["record", "stop", "play"]).describe("Action to perform with macros"),
486 |     register: z.string().optional().describe("Register to record/play macro (a-z, required for record/play)"),
487 |     count: z.number().optional().describe("Number of times to play macro (default: 1)")
488 |   },
489 |   async ({ action, register, count = 1 }) => {
490 |     try {
491 |       const result = await neovimManager.manageMacro(action, register, count);
492 |       return {
493 |         content: [{
494 |           type: "text",
495 |           text: result
496 |         }]
497 |       };
498 |     } catch (error) {
499 |       return {
500 |         content: [{
501 |           type: "text",
502 |           text: error instanceof Error ? error.message : 'Error managing macro'
503 |         }]
504 |       };
505 |     }
506 |   }
507 | );
508 | 
509 | // Tab management tool
510 | server.tool(
511 |   "vim_tab",
512 |   "Manage Neovim tabs: create, close, and navigate between tabs",
513 |   {
514 |     action: z.enum(["new", "close", "next", "prev", "first", "last", "list"]).describe("Tab action to perform"),
515 |     filename: z.string().optional().describe("Filename for new tab (optional)")
516 |   },
517 |   async ({ action, filename }) => {
518 |     try {
519 |       const result = await neovimManager.manageTab(action, filename);
520 |       return {
521 |         content: [{
522 |           type: "text",
523 |           text: result
524 |         }]
525 |       };
526 |     } catch (error) {
527 |       return {
528 |         content: [{
529 |           type: "text",
530 |           text: error instanceof Error ? error.message : 'Error managing tab'
531 |         }]
532 |       };
533 |     }
534 |   }
535 | );
536 | 
537 | // Code folding tool
538 | server.tool(
539 |   "vim_fold",
540 |   "Manage code folding: create, open, close, and toggle folds",
541 |   {
542 |     action: z.enum(["create", "open", "close", "toggle", "openall", "closeall", "delete"]).describe("Folding action to perform"),
543 |     startLine: z.number().optional().describe("Start line for creating fold (required for create)"),
544 |     endLine: z.number().optional().describe("End line for creating fold (required for create)")
545 |   },
546 |   async ({ action, startLine, endLine }) => {
547 |     try {
548 |       const result = await neovimManager.manageFold(action, startLine, endLine);
549 |       return {
550 |         content: [{
551 |           type: "text",
552 |           text: result
553 |         }]
554 |       };
555 |     } catch (error) {
556 |       return {
557 |         content: [{
558 |           type: "text",
559 |           text: error instanceof Error ? error.message : 'Error managing fold'
560 |         }]
561 |       };
562 |     }
563 |   }
564 | );
565 | 
566 | // Jump list navigation tool
567 | server.tool(
568 |   "vim_jump",
569 |   "Navigate Neovim jump list: go back, forward, or list jumps",
570 |   {
571 |     direction: z.enum(["back", "forward", "list"]).describe("Jump direction or list jumps")
572 |   },
573 |   async ({ direction }) => {
574 |     try {
575 |       const result = await neovimManager.navigateJumpList(direction);
576 |       return {
577 |         content: [{
578 |           type: "text",
579 |           text: result
580 |         }]
581 |       };
582 |     } catch (error) {
583 |       return {
584 |         content: [{
585 |           type: "text",
586 |           text: error instanceof Error ? error.message : 'Error navigating jump list'
587 |         }]
588 |       };
589 |     }
590 |   }
591 | );
592 | 
593 | // Register a sample prompt for Neovim workflow assistance
594 | server.prompt(
595 |   "neovim_workflow", 
596 |   "Get help with common Neovim workflows and editing tasks",
597 |   {
598 |     task: z.enum(["editing", "navigation", "search", "buffers", "windows", "macros"]).describe("Type of Neovim task you need help with")
599 |   },
600 |   async ({ task }) => {
601 |     const workflows = {
602 |       editing: "Here are common editing workflows:\n1. Use vim_edit with 'insert' mode to add new content\n2. Use vim_edit with 'replace' mode to modify existing lines\n3. Use vim_search_replace for find and replace operations\n4. Use vim_visual to select text ranges before operations",
603 |       navigation: "Navigation workflows:\n1. Use vim_mark to set bookmarks in your code\n2. Use vim_jump to navigate through your jump history\n3. Use vim_command with 'gg' or 'G' to go to start/end of file\n4. Use vim_command with line numbers like ':42' to jump to specific lines",
604 |       search: "Search workflows:\n1. Use vim_search to find patterns in current buffer\n2. Use vim_grep for project-wide searches\n3. Use vim_search_replace for complex find/replace operations\n4. Use regex patterns for advanced matching",
605 |       buffers: "Buffer management:\n1. Use vim_buffer to view buffer contents\n2. Use vim_buffer_switch to change between buffers\n3. Use vim_file_open to open new files\n4. Use vim_buffer_save to save your work",
606 |       windows: "Window management:\n1. Use vim_window with 'split'/'vsplit' to create new windows\n2. Use vim_window with 'wincmd h/j/k/l' to navigate between windows\n3. Use vim_window with 'close' to close current window\n4. Use vim_window with 'only' to keep only current window",
607 |       macros: "Macro workflows:\n1. Use vim_macro with 'record' and a register to start recording\n2. Perform your actions in Neovim\n3. Use vim_macro with 'stop' to end recording\n4. Use vim_macro with 'play' to execute recorded actions"
608 |     };
609 | 
610 |     return {
611 |       messages: [
612 |         {
613 |           role: "assistant",
614 |           content: {
615 |             type: "text",
616 |             text: workflows[task] || "Unknown task type. Available tasks: editing, navigation, search, buffers, windows, macros"
617 |           }
618 |         }
619 |       ]
620 |     };
621 |   }
622 | );
623 | 
624 | /**
625 |  * Start the server using stdio transport.
626 |  * This allows the server to communicate via standard input/output streams.
627 |  */
628 | async function main() {
629 |   const transport = new StdioServerTransport();
630 |   await server.connect(transport);
631 | }
632 | 
633 | main().catch((error) => {
634 |   console.error("Server error:", error);
635 |   process.exit(1);
636 | });
637 | 
```

--------------------------------------------------------------------------------
/src/neovim.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { attach, Neovim } from 'neovim';
  2 | 
  3 | export class NeovimConnectionError extends Error {
  4 |   constructor(socketPath: string, cause?: Error) {
  5 |     super(`Failed to connect to Neovim at ${socketPath}. Is Neovim running with --listen ${socketPath}?`);
  6 |     this.name = 'NeovimConnectionError';
  7 |     this.cause = cause;
  8 |   }
  9 | }
 10 | 
 11 | export class NeovimCommandError extends Error {
 12 |   constructor(command: string, originalError: string) {
 13 |     super(`Failed to execute command '${command}': ${originalError}`);
 14 |     this.name = 'NeovimCommandError';
 15 |   }
 16 | }
 17 | 
 18 | export class NeovimValidationError extends Error {
 19 |   constructor(message: string) {
 20 |     super(message);
 21 |     this.name = 'NeovimValidationError';
 22 |   }
 23 | }
 24 | 
 25 | interface NeovimStatus {
 26 |   cursorPosition: [number, number];
 27 |   mode: string;
 28 |   visualSelection: string;
 29 |   fileName: string;
 30 |   windowLayout: string;
 31 |   currentTab: number;
 32 |   marks: { [key: string]: [number, number] };
 33 |   registers: { [key: string]: string };
 34 |   cwd: string;
 35 |   lspInfo?: string;
 36 |   pluginInfo?: string;
 37 |   visualInfo?: {
 38 |     hasActiveSelection: boolean;
 39 |     visualModeType?: string;
 40 |     startPos?: [number, number];
 41 |     endPos?: [number, number];
 42 |     lastVisualStart?: [number, number];
 43 |     lastVisualEnd?: [number, number];
 44 |   };
 45 | }
 46 | 
 47 | interface BufferInfo {
 48 |   number: number;
 49 |   name: string;
 50 |   isListed: boolean;
 51 |   isLoaded: boolean;
 52 |   modified: boolean;
 53 |   syntax: string;
 54 |   windowIds: number[];
 55 | }
 56 | 
 57 | interface WindowInfo {
 58 |   id: number;
 59 |   bufferId: number;
 60 |   width: number;
 61 |   height: number;
 62 |   row: number;
 63 |   col: number;
 64 | }
 65 | 
 66 | export class NeovimManager {
 67 |   private static instance: NeovimManager;
 68 | 
 69 |   private constructor() { }
 70 | 
 71 |   public static getInstance(): NeovimManager {
 72 |     if (!NeovimManager.instance) {
 73 |       NeovimManager.instance = new NeovimManager();
 74 |     }
 75 |     return NeovimManager.instance;
 76 |   }
 77 | 
 78 |   public async healthCheck(): Promise<boolean> {
 79 |     try {
 80 |       const nvim = await this.connect();
 81 |       await nvim.eval('1'); // Simple test
 82 |       return true;
 83 |     } catch {
 84 |       return false;
 85 |     }
 86 |   }
 87 | 
 88 |   private validateSocketPath(path: string): void {
 89 |     if (!path || path.trim().length === 0) {
 90 |       throw new NeovimValidationError('Socket path cannot be empty');
 91 |     }
 92 |   }
 93 | 
 94 |   private async connect(): Promise<Neovim> {
 95 |     const socketPath = process.env.NVIM_SOCKET_PATH || '/tmp/nvim';
 96 |     this.validateSocketPath(socketPath);
 97 |     
 98 |     try {
 99 |       return attach({
100 |         socket: socketPath
101 |       });
102 |     } catch (error) {
103 |       console.error('Error connecting to Neovim:', error);
104 |       throw new NeovimConnectionError(socketPath, error as Error);
105 |     }
106 |   }
107 | 
108 |   public async getBufferContents(filename?: string): Promise<Map<number, string>> {
109 |     try {
110 |       const nvim = await this.connect();
111 |       let buffer;
112 |       
113 |       if (filename) {
114 |         // Find buffer by filename
115 |         const buffers = await nvim.buffers;
116 |         let targetBuffer = null;
117 |         
118 |         for (const buf of buffers) {
119 |           const bufName = await buf.name;
120 |           if (bufName === filename || bufName.endsWith(filename)) {
121 |             targetBuffer = buf;
122 |             break;
123 |           }
124 |         }
125 |         
126 |         if (!targetBuffer) {
127 |           throw new NeovimValidationError(`Buffer not found: ${filename}`);
128 |         }
129 |         buffer = targetBuffer;
130 |       } else {
131 |         buffer = await nvim.buffer;
132 |       }
133 |       
134 |       const lines = await buffer.lines;
135 |       const lineMap = new Map<number, string>();
136 | 
137 |       lines.forEach((line: string, index: number) => {
138 |         lineMap.set(index + 1, line);
139 |       });
140 | 
141 |       return lineMap;
142 |     } catch (error) {
143 |       if (error instanceof NeovimValidationError) {
144 |         throw error;
145 |       }
146 |       console.error('Error getting buffer contents:', error);
147 |       return new Map();
148 |     }
149 |   }
150 | 
151 |   public async sendCommand(command: string): Promise<string> {
152 |     if (!command || command.trim().length === 0) {
153 |       throw new NeovimValidationError('Command cannot be empty');
154 |     }
155 | 
156 |     try {
157 |       const nvim = await this.connect();
158 | 
159 |       // Remove leading colon if present
160 |       const normalizedCommand = command.startsWith(':') ? command.substring(1) : command;
161 | 
162 |       // Handle shell commands (starting with !)
163 |       if (normalizedCommand.startsWith('!')) {
164 |         if (process.env.ALLOW_SHELL_COMMANDS !== 'true') {
165 |           return 'Shell command execution is disabled. Set ALLOW_SHELL_COMMANDS=true environment variable to enable shell commands.';
166 |         }
167 | 
168 |         const shellCommand = normalizedCommand.substring(1).trim();
169 |         if (!shellCommand) {
170 |           throw new NeovimValidationError('Shell command cannot be empty');
171 |         }
172 |         
173 |         try {
174 |           // Execute the command and capture output directly
175 |           const output = await nvim.eval(`system('${shellCommand.replace(/'/g, "''")}')`);
176 |           if (output) {
177 |             return String(output).trim();
178 |           }
179 |           return 'No output from command';
180 |         } catch (error) {
181 |           console.error('Shell command error:', error);
182 |           const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
183 |           throw new NeovimCommandError(`!${shellCommand}`, errorMessage);
184 |         }
185 |       }
186 | 
187 |       // For regular Vim commands
188 |       await nvim.setVvar('errmsg', '');
189 |       
190 |       // Execute the command and capture its output using the execute() function
191 |       const output = await nvim.call('execute', [normalizedCommand]);
192 |       
193 |       // Check for errors
194 |       const vimerr = await nvim.getVvar('errmsg');
195 |       if (vimerr) {
196 |         console.error('Vim error:', vimerr);
197 |         throw new NeovimCommandError(normalizedCommand, String(vimerr));
198 |       }
199 | 
200 |       // Return the actual command output if any
201 |       return output ? String(output).trim() : 'Command executed (no output)';
202 |     } catch (error) {
203 |       if (error instanceof NeovimCommandError || error instanceof NeovimValidationError) {
204 |         throw error;
205 |       }
206 |       console.error('Error sending command:', error);
207 |       throw new NeovimCommandError(command, error instanceof Error ? error.message : 'Unknown error');
208 |     }
209 |   }
210 | 
211 |   private async getVisualSelectionInfo(nvim: Neovim, mode: string): Promise<{
212 |     hasSelection: boolean;
213 |     selectedText?: string;
214 |     startPos?: [number, number];
215 |     endPos?: [number, number];
216 |     visualModeType?: string;
217 |     lastVisualStart?: [number, number];
218 |     lastVisualEnd?: [number, number];
219 |   }> {
220 |     try {
221 |       const isInVisualMode = mode.includes('v') || mode.includes('V') || mode.includes('\x16');
222 | 
223 |       if (isInVisualMode) {
224 |         // Currently in visual mode - get active selection
225 |         const [startPos, endPos, initialVisualModeType] = await Promise.all([
226 |           nvim.call('getpos', ['v']) as Promise<[number, number, number, number]>,
227 |           nvim.call('getpos', ['.']) as Promise<[number, number, number, number]>,
228 |           nvim.call('visualmode', []) as Promise<string>
229 |         ]);
230 | 
231 |         // Convert positions to [line, col] format
232 |         const start: [number, number] = [startPos[1], startPos[2]];
233 |         const end: [number, number] = [endPos[1], endPos[2]];
234 | 
235 |         // Get the selected text using a more reliable approach
236 |         let selectedText = '';
237 |         let visualModeType = initialVisualModeType;
238 |         try {
239 |           const result = await nvim.lua(`
240 |             -- Get visual mode type first
241 |             local mode = vim.fn.visualmode()
242 |             if not mode or mode == '' then
243 |               return { text = '', mode = '' }
244 |             end
245 | 
246 |             local start_pos = vim.fn.getpos('v')
247 |             local end_pos = vim.fn.getpos('.')
248 |             local start_line, start_col = start_pos[2], start_pos[3]
249 |             local end_line, end_col = end_pos[2], end_pos[3]
250 | 
251 |             -- Ensure proper ordering (start should be before end)
252 |             if start_line > end_line or (start_line == end_line and start_col > end_col) then
253 |               start_line, end_line = end_line, start_line
254 |               start_col, end_col = end_col, start_col
255 |             end
256 | 
257 |             local text = ''
258 | 
259 |             if mode == 'v' then
260 |               -- Character-wise visual mode
261 |               if start_line == end_line then
262 |                 local line = vim.api.nvim_buf_get_lines(0, start_line - 1, start_line, false)[1] or ''
263 |                 text = line:sub(start_col, end_col)
264 |               else
265 |                 local lines = vim.api.nvim_buf_get_lines(0, start_line - 1, end_line, false)
266 |                 if #lines > 0 then
267 |                   -- Handle first line
268 |                   lines[1] = lines[1]:sub(start_col)
269 |                   -- Handle last line
270 |                   if #lines > 1 then
271 |                     lines[#lines] = lines[#lines]:sub(1, end_col)
272 |                   end
273 |                   text = table.concat(lines, '\\n')
274 |                 end
275 |               end
276 |             elseif mode == 'V' then
277 |               -- Line-wise visual mode
278 |               local lines = vim.api.nvim_buf_get_lines(0, start_line - 1, end_line, false)
279 |               text = table.concat(lines, '\\n')
280 |             elseif mode == '\\022' then
281 |               -- Block-wise visual mode (Ctrl-V)
282 |               local lines = vim.api.nvim_buf_get_lines(0, start_line - 1, end_line, false)
283 |               local result = {}
284 |               for _, line in ipairs(lines) do
285 |                 table.insert(result, line:sub(start_col, end_col))
286 |               end
287 |               text = table.concat(result, '\\n')
288 |             end
289 | 
290 |             return { text = text, mode = mode }
291 |           `) as { text: string; mode: string };
292 | 
293 |           selectedText = result.text || '';
294 |           visualModeType = result.mode || visualModeType;
295 |         } catch (e) {
296 |           selectedText = '[Selection text unavailable]';
297 |         }
298 | 
299 |         return {
300 |           hasSelection: true,
301 |           selectedText,
302 |           startPos: start,
303 |           endPos: end,
304 |           visualModeType
305 |         };
306 |       } else {
307 |         // Not in visual mode - get last visual selection marks
308 |         try {
309 |           const [lastStart, lastEnd] = await Promise.all([
310 |             nvim.call('getpos', ["'<"]) as Promise<[number, number, number, number]>,
311 |             nvim.call('getpos', ["'>"]) as Promise<[number, number, number, number]>
312 |           ]);
313 | 
314 |           return {
315 |             hasSelection: false,
316 |             lastVisualStart: [lastStart[1], lastStart[2]],
317 |             lastVisualEnd: [lastEnd[1], lastEnd[2]]
318 |           };
319 |         } catch (e) {
320 |           return { hasSelection: false };
321 |         }
322 |       }
323 |     } catch (error) {
324 |       return { hasSelection: false, selectedText: `Error: ${error instanceof Error ? error.message : 'Unknown error'}` };
325 |     }
326 |   }
327 | 
328 |   public async getNeovimStatus(): Promise<NeovimStatus | string> {
329 |     try {
330 |       const nvim = await this.connect();
331 |       const window = await nvim.window;
332 |       const cursor = await window.cursor;
333 |       const mode = await nvim.mode;
334 |       const buffer = await nvim.buffer;
335 |       
336 |       // Get window layout
337 |       const layout = await nvim.eval('winlayout()');
338 |       const tabpage = await nvim.tabpage;
339 |       const currentTab = await tabpage.number;
340 | 
341 |       // Get marks (a-z) - only include set marks
342 |       const marks: { [key: string]: [number, number] } = {};
343 |       for (const mark of 'abcdefghijklmnopqrstuvwxyz') {
344 |         try {
345 |           const pos = await nvim.eval(`getpos("'${mark}")`) as [number, number, number, number];
346 |           // Only include marks that are actually set (not at position 0,0)
347 |           if (pos[1] > 0 && pos[2] > 0) {
348 |             marks[mark] = [pos[1], pos[2]];
349 |           }
350 |         } catch (e) {
351 |           // Mark not set
352 |         }
353 |       }
354 | 
355 |       // Get registers (a-z, ", 0-9) - only include non-empty registers
356 |       const registers: { [key: string]: string } = {};
357 |       const registerNames = [...'abcdefghijklmnopqrstuvwxyz', '"', ...Array(10).keys()];
358 |       for (const reg of registerNames) {
359 |         try {
360 |           const content = String(await nvim.eval(`getreg('${reg}')`));
361 |           // Only include registers that have content
362 |           if (content && content.trim().length > 0) {
363 |             registers[String(reg)] = content;
364 |           }
365 |         } catch (e) {
366 |           // Register empty or error
367 |         }
368 |       }
369 | 
370 |       // Get current working directory
371 |       const cwd = await nvim.call('getcwd');
372 | 
373 |       // Get basic plugin information (LSP clients, loaded plugins)
374 |       let lspInfo = '';
375 |       let pluginInfo = '';
376 |       
377 |       try {
378 |         // Get LSP clients if available (use new API for Neovim >=0.10)
379 |         const lspClients = await nvim.eval('luaeval("vim.lsp.get_clients()")');
380 |         if (Array.isArray(lspClients) && lspClients.length > 0) {
381 |           const clientNames = lspClients.map((client: any) => client.name || 'unknown').join(', ');
382 |           lspInfo = `Active LSP clients: ${clientNames}`;
383 |         } else {
384 |           lspInfo = 'No active LSP clients';
385 |         }
386 |       } catch (e) {
387 |         lspInfo = 'LSP information unavailable';
388 |       }
389 | 
390 |       try {
391 |         // Get loaded plugins (simplified check)
392 |         const hasLsp = await nvim.eval('exists(":LspInfo")');
393 |         const hasTelescope = await nvim.eval('exists(":Telescope")');
394 |         const hasTreesitter = await nvim.eval('exists("g:loaded_nvim_treesitter")');
395 |         const hasCompletion = await nvim.eval('exists("g:loaded_completion")');
396 |         
397 |         const plugins = [];
398 |         if (hasLsp) plugins.push('LSP');
399 |         if (hasTelescope) plugins.push('Telescope');
400 |         if (hasTreesitter) plugins.push('TreeSitter');
401 |         if (hasCompletion) plugins.push('Completion');
402 |         
403 |         pluginInfo = plugins.length > 0 ? `Detected plugins: ${plugins.join(', ')}` : 'No common plugins detected';
404 |       } catch (e) {
405 |         pluginInfo = 'Plugin information unavailable';
406 |       }
407 | 
408 |       // Get visual selection information using the new method
409 |       const visualInfo = await this.getVisualSelectionInfo(nvim, mode.mode);
410 | 
411 |       const neovimStatus: NeovimStatus = {
412 |         cursorPosition: cursor,
413 |         mode: mode.mode,
414 |         visualSelection: visualInfo.selectedText || '',
415 |         fileName: await buffer.name,
416 |         windowLayout: JSON.stringify(layout),
417 |         currentTab,
418 |         marks,
419 |         registers,
420 |         cwd,
421 |         lspInfo,
422 |         pluginInfo,
423 |         visualInfo: {
424 |           hasActiveSelection: visualInfo.hasSelection,
425 |           visualModeType: visualInfo.visualModeType,
426 |           startPos: visualInfo.startPos,
427 |           endPos: visualInfo.endPos,
428 |           lastVisualStart: visualInfo.lastVisualStart,
429 |           lastVisualEnd: visualInfo.lastVisualEnd
430 |         }
431 |       };
432 | 
433 |       return neovimStatus;
434 |     } catch (error) {
435 |       console.error('Error getting Neovim status:', error);
436 |       return 'Error getting Neovim status';
437 |     }
438 |   }
439 | 
440 |   public async editLines(startLine: number, mode: 'replace' | 'insert' | 'replaceAll', newText: string): Promise<string> {
441 |     try {
442 |       const nvim = await this.connect();
443 |       const splitByLines = newText.split('\n');
444 |       const buffer = await nvim.buffer;
445 | 
446 |       if (mode === 'replaceAll') {
447 |         // Handle full buffer replacement
448 |         const lineCount = await buffer.length;
449 |         // Delete all lines and then append new content
450 |         await buffer.remove(0, lineCount, true);
451 |         await buffer.insert(splitByLines, 0);
452 |         return 'Buffer completely replaced';
453 |       } else if (mode === 'replace') {
454 |         await buffer.replace(splitByLines, startLine - 1);
455 |         return 'Lines replaced successfully';
456 |       } else if (mode === 'insert') {
457 |         await buffer.insert(splitByLines, startLine - 1);
458 |         return 'Lines inserted successfully';
459 |       }
460 | 
461 |       return 'Invalid mode specified';
462 |     } catch (error) {
463 |       console.error('Error editing lines:', error);
464 |       return 'Error editing lines';
465 |     }
466 |   }
467 | 
468 |   public async getWindows(): Promise<WindowInfo[]> {
469 |     try {
470 |       const nvim = await this.connect();
471 |       const windows = await nvim.windows;
472 |       const windowInfos: WindowInfo[] = [];
473 | 
474 |       for (const win of windows) {
475 |         const buffer = await win.buffer;
476 |         const [width, height] = await Promise.all([
477 |           win.width,
478 |           win.height
479 |         ]);
480 |         const position = await win.position;
481 | 
482 |         windowInfos.push({
483 |           id: win.id,
484 |           bufferId: buffer.id,
485 |           width,
486 |           height,
487 |           row: position[0],
488 |           col: position[1]
489 |         });
490 |       }
491 | 
492 |       return windowInfos;
493 |     } catch (error) {
494 |       console.error('Error getting windows:', error);
495 |       return [];
496 |     }
497 |   }
498 | 
499 |   public async manipulateWindow(command: string): Promise<string> {
500 |     const validCommands = ['split', 'vsplit', 'only', 'close', 'wincmd h', 'wincmd j', 'wincmd k', 'wincmd l'];
501 |     if (!validCommands.some(cmd => command.startsWith(cmd))) {
502 |       return 'Invalid window command';
503 |     }
504 | 
505 |     try {
506 |       const nvim = await this.connect();
507 |       await nvim.command(command);
508 |       return 'Window command executed';
509 |     } catch (error) {
510 |       console.error('Error manipulating window:', error);
511 |       return 'Error executing window command';
512 |     }
513 |   }
514 | 
515 |   public async setMark(mark: string, line: number, col: number): Promise<string> {
516 |     if (!/^[a-z]$/.test(mark)) {
517 |       return 'Invalid mark name (must be a-z)';
518 |     }
519 | 
520 |     try {
521 |       const nvim = await this.connect();
522 |       await nvim.command(`mark ${mark}`);
523 |       const window = await nvim.window;
524 |       await (window.cursor = [line, col]);
525 |       return `Mark ${mark} set at line ${line}, column ${col}`;
526 |     } catch (error) {
527 |       console.error('Error setting mark:', error);
528 |       return 'Error setting mark';
529 |     }
530 |   }
531 | 
532 |   public async setRegister(register: string, content: string): Promise<string> {
533 |     const validRegisters = [...'abcdefghijklmnopqrstuvwxyz"'];
534 |     if (!validRegisters.includes(register)) {
535 |       return 'Invalid register name';
536 |     }
537 | 
538 |     try {
539 |       const nvim = await this.connect();
540 |       await nvim.eval(`setreg('${register}', '${content.replace(/'/g, "''")}')`);
541 |       return `Register ${register} set`;
542 |     } catch (error) {
543 |       console.error('Error setting register:', error);
544 |       return 'Error setting register';
545 |     }
546 |   }
547 | 
548 |   public async visualSelect(startLine: number, startCol: number, endLine: number, endCol: number): Promise<string> {
549 |     try {
550 |       const nvim = await this.connect();
551 |       const window = await nvim.window;
552 |       
553 |       // Enter visual mode
554 |       await nvim.command('normal! v');
555 |       
556 |       // Move cursor to start position
557 |       await (window.cursor = [startLine, startCol]);
558 |       
559 |       // Move cursor to end position (selection will be made)
560 |       await (window.cursor = [endLine, endCol]);
561 |       
562 |       return 'Visual selection made';
563 |     } catch (error) {
564 |       console.error('Error making visual selection:', error);
565 |       return 'Error making visual selection';
566 |     }
567 |   }
568 | 
569 |   public async switchBuffer(identifier: string | number): Promise<string> {
570 |     try {
571 |       const nvim = await this.connect();
572 |       
573 |       // If identifier is a number, switch by buffer number
574 |       if (typeof identifier === 'number') {
575 |         await nvim.command(`buffer ${identifier}`);
576 |         return `Switched to buffer ${identifier}`;
577 |       }
578 |       
579 |       // If identifier is a string, try to find buffer by name
580 |       const buffers = await nvim.buffers;
581 |       for (const buffer of buffers) {
582 |         const bufName = await buffer.name;
583 |         if (bufName === identifier || bufName.endsWith(identifier)) {
584 |           await nvim.command(`buffer ${buffer.id}`);
585 |           return `Switched to buffer: ${bufName}`;
586 |         }
587 |       }
588 |       
589 |       throw new NeovimValidationError(`Buffer not found: ${identifier}`);
590 |     } catch (error) {
591 |       if (error instanceof NeovimValidationError) {
592 |         throw error;
593 |       }
594 |       console.error('Error switching buffer:', error);
595 |       throw new NeovimCommandError(`buffer switch to ${identifier}`, error instanceof Error ? error.message : 'Unknown error');
596 |     }
597 |   }
598 | 
599 |   public async saveBuffer(filename?: string): Promise<string> {
600 |     try {
601 |       const nvim = await this.connect();
602 |       
603 |       if (filename) {
604 |         // Save with specific filename
605 |         await nvim.command(`write ${filename}`);
606 |         return `Buffer saved to: ${filename}`;
607 |       } else {
608 |         // Save current buffer
609 |         const buffer = await nvim.buffer;
610 |         const bufferName = await buffer.name;
611 |         
612 |         if (!bufferName) {
613 |           throw new NeovimValidationError('Cannot save unnamed buffer without specifying filename');
614 |         }
615 |         
616 |         await nvim.command('write');
617 |         return `Buffer saved: ${bufferName}`;
618 |       }
619 |     } catch (error) {
620 |       if (error instanceof NeovimValidationError) {
621 |         throw error;
622 |       }
623 |       console.error('Error saving buffer:', error);
624 |       throw new NeovimCommandError(`save ${filename || 'current buffer'}`, error instanceof Error ? error.message : 'Unknown error');
625 |     }
626 |   }
627 | 
628 |   public async openFile(filename: string): Promise<string> {
629 |     if (!filename || filename.trim().length === 0) {
630 |       throw new NeovimValidationError('Filename cannot be empty');
631 |     }
632 |     
633 |     try {
634 |       const nvim = await this.connect();
635 |       await nvim.command(`edit ${filename}`);
636 |       return `Opened file: ${filename}`;
637 |     } catch (error) {
638 |       console.error('Error opening file:', error);
639 |       throw new NeovimCommandError(`edit ${filename}`, error instanceof Error ? error.message : 'Unknown error');
640 |     }
641 |   }
642 | 
643 |   public async searchInBuffer(pattern: string, options: { ignoreCase?: boolean; wholeWord?: boolean } = {}): Promise<string> {
644 |     if (!pattern || pattern.trim().length === 0) {
645 |       throw new NeovimValidationError('Search pattern cannot be empty');
646 |     }
647 |     
648 |     try {
649 |       const nvim = await this.connect();
650 |       
651 |       // Build search command with options
652 |       let searchPattern = pattern;
653 |       if (options.wholeWord) {
654 |         searchPattern = `\\<${pattern}\\>`;
655 |       }
656 |       
657 |       // Set search options
658 |       if (options.ignoreCase) {
659 |         await nvim.command('set ignorecase');
660 |       } else {
661 |         await nvim.command('set noignorecase');
662 |       }
663 |       
664 |       // Perform search and get matches
665 |       const matches = await nvim.eval(`searchcount({"pattern": "${searchPattern.replace(/"/g, '\\"')}", "maxcount": 100})`);
666 |       const matchInfo = matches as { current: number; total: number; maxcount: number; incomplete: number };
667 |       
668 |       if (matchInfo.total === 0) {
669 |         return `No matches found for: ${pattern}`;
670 |       }
671 |       
672 |       // Move to first match
673 |       await nvim.command(`/${searchPattern}`);
674 |       
675 |       return `Found ${matchInfo.total} matches for: ${pattern}${matchInfo.incomplete ? ' (showing first 100)' : ''}`;
676 |     } catch (error) {
677 |       console.error('Error searching in buffer:', error);
678 |       throw new NeovimCommandError(`search for ${pattern}`, error instanceof Error ? error.message : 'Unknown error');
679 |     }
680 |   }
681 | 
682 |   public async searchAndReplace(pattern: string, replacement: string, options: { global?: boolean; ignoreCase?: boolean; confirm?: boolean } = {}): Promise<string> {
683 |     if (!pattern || pattern.trim().length === 0) {
684 |       throw new NeovimValidationError('Search pattern cannot be empty');
685 |     }
686 |     
687 |     try {
688 |       const nvim = await this.connect();
689 |       
690 |       // Build substitute command
691 |       let flags = '';
692 |       if (options.global) flags += 'g';
693 |       if (options.ignoreCase) flags += 'i';
694 |       if (options.confirm) flags += 'c';
695 |       
696 |       const command = `%s/${pattern.replace(/\//g, '\\/')}/${replacement.replace(/\//g, '\\/')}/${flags}`;
697 |       
698 |       const result = await nvim.call('execute', [command]);
699 |       return result ? String(result).trim() : 'Search and replace completed';
700 |     } catch (error) {
701 |       console.error('Error in search and replace:', error);
702 |       throw new NeovimCommandError(`substitute ${pattern} -> ${replacement}`, error instanceof Error ? error.message : 'Unknown error');
703 |     }
704 |   }
705 | 
706 |   public async grepInProject(pattern: string, filePattern: string = '**/*'): Promise<string> {
707 |     if (!pattern || pattern.trim().length === 0) {
708 |       throw new NeovimValidationError('Grep pattern cannot be empty');
709 |     }
710 |     
711 |     try {
712 |       const nvim = await this.connect();
713 |       
714 |       // Use vimgrep for internal searching
715 |       const command = `vimgrep /${pattern}/ ${filePattern}`;
716 |       await nvim.command(command);
717 |       
718 |       // Get quickfix list
719 |       const qflist = await nvim.eval('getqflist()');
720 |       const results = qflist as Array<{ filename: string; lnum: number; text: string }>;
721 |       
722 |       if (results.length === 0) {
723 |         return `No matches found for: ${pattern}`;
724 |       }
725 |       
726 |       const summary = results.slice(0, 10).map(item => 
727 |         `${item.filename}:${item.lnum}: ${item.text.trim()}`
728 |       ).join('\n');
729 |       
730 |       const totalText = results.length > 10 ? `\n... and ${results.length - 10} more matches` : '';
731 |       return `Found ${results.length} matches for: ${pattern}\n${summary}${totalText}`;
732 |     } catch (error) {
733 |       console.error('Error in grep:', error);
734 |       throw new NeovimCommandError(`grep ${pattern}`, error instanceof Error ? error.message : 'Unknown error');
735 |     }
736 |   }
737 | 
738 |   public async getOpenBuffers(): Promise<BufferInfo[]> {
739 |     try {
740 |       const nvim = await this.connect();
741 |       const buffers = await nvim.buffers;
742 |       const windows = await nvim.windows;
743 |       const bufferInfos: BufferInfo[] = [];
744 | 
745 |       for (const buffer of buffers) {
746 |         const [
747 |           isLoaded,
748 |           isListedOption,
749 |           modified,
750 |           syntax
751 |         ] = await Promise.all([
752 |           buffer.loaded,
753 |           buffer.getOption('buflisted'),
754 |           buffer.getOption('modified'),
755 |           buffer.getOption('syntax')
756 |         ]);
757 |         const isListed = Boolean(isListedOption);
758 | 
759 |         // Find windows containing this buffer
760 |         const windowIds = [];
761 |         for (const win of windows) {
762 |           const winBuffer = await win.buffer;
763 |           if (winBuffer.id === buffer.id) {
764 |             windowIds.push(win.id);
765 |           }
766 |         }
767 | 
768 |         bufferInfos.push({
769 |           number: buffer.id,
770 |           name: await buffer.name,
771 |           isListed,
772 |           isLoaded,
773 |           modified: Boolean(modified),
774 |           syntax: String(syntax),
775 |           windowIds
776 |         });
777 |       }
778 | 
779 |       return bufferInfos;
780 |     } catch (error) {
781 |       console.error('Error getting open buffers:', error);
782 |       return [];
783 |     }
784 |   }
785 | 
786 |   public async manageMacro(action: string, register?: string, count: number = 1): Promise<string> {
787 |     try {
788 |       const nvim = await this.connect();
789 |       
790 |       switch (action) {
791 |         case 'record':
792 |           if (!register || register.length !== 1 || !/[a-z]/.test(register)) {
793 |             throw new NeovimValidationError('Register must be a single letter a-z for recording');
794 |           }
795 |           await nvim.input(`q${register}`);
796 |           return `Started recording macro in register '${register}'`;
797 |           
798 |         case 'stop':
799 |           await nvim.input('q');
800 |           return 'Stopped recording macro';
801 |           
802 |         case 'play':
803 |           if (!register || register.length !== 1 || !/[a-z]/.test(register)) {
804 |             throw new NeovimValidationError('Register must be a single letter a-z for playing');
805 |           }
806 |           const playCommand = count > 1 ? `${count}@${register}` : `@${register}`;
807 |           await nvim.input(playCommand);
808 |           return `Played macro from register '${register}' ${count} time(s)`;
809 |           
810 |         default:
811 |           throw new NeovimValidationError(`Unknown macro action: ${action}`);
812 |       }
813 |     } catch (error) {
814 |       if (error instanceof NeovimValidationError) {
815 |         throw error;
816 |       }
817 |       console.error('Error managing macro:', error);
818 |       throw new NeovimCommandError(`macro ${action}`, error instanceof Error ? error.message : 'Unknown error');
819 |     }
820 |   }
821 | 
822 |   public async manageTab(action: string, filename?: string): Promise<string> {
823 |     try {
824 |       const nvim = await this.connect();
825 |       
826 |       switch (action) {
827 |         case 'new':
828 |           if (filename) {
829 |             await nvim.command(`tabnew ${filename}`);
830 |             return `Created new tab with file: ${filename}`;
831 |           } else {
832 |             await nvim.command('tabnew');
833 |             return 'Created new empty tab';
834 |           }
835 |           
836 |         case 'close':
837 |           await nvim.command('tabclose');
838 |           return 'Closed current tab';
839 |           
840 |         case 'next':
841 |           await nvim.command('tabnext');
842 |           return 'Moved to next tab';
843 |           
844 |         case 'prev':
845 |           await nvim.command('tabprev');
846 |           return 'Moved to previous tab';
847 |           
848 |         case 'first':
849 |           await nvim.command('tabfirst');
850 |           return 'Moved to first tab';
851 |           
852 |         case 'last':
853 |           await nvim.command('tablast');
854 |           return 'Moved to last tab';
855 |           
856 |         case 'list':
857 |           const tabs = await nvim.tabpages;
858 |           const tabInfo = [];
859 |           for (let i = 0; i < tabs.length; i++) {
860 |             const tab = tabs[i];
861 |             const win = await tab.window;
862 |             const buf = await win.buffer;
863 |             const name = await buf.name;
864 |             const current = await nvim.tabpage;
865 |             const isCurrent = tab === current;
866 |             tabInfo.push(`${isCurrent ? '*' : ' '}${i + 1}: ${name || '[No Name]'}`);
867 |           }
868 |           return `Tabs:\n${tabInfo.join('\n')}`;
869 |           
870 |         default:
871 |           throw new NeovimValidationError(`Unknown tab action: ${action}`);
872 |       }
873 |     } catch (error) {
874 |       if (error instanceof NeovimValidationError) {
875 |         throw error;
876 |       }
877 |       console.error('Error managing tab:', error);
878 |       throw new NeovimCommandError(`tab ${action}`, error instanceof Error ? error.message : 'Unknown error');
879 |     }
880 |   }
881 | 
882 |   public async manageFold(action: string, startLine?: number, endLine?: number): Promise<string> {
883 |     try {
884 |       const nvim = await this.connect();
885 |       
886 |       switch (action) {
887 |         case 'create':
888 |           if (startLine === undefined || endLine === undefined) {
889 |             throw new NeovimValidationError('Start line and end line are required for creating folds');
890 |           }
891 |           await nvim.command(`${startLine},${endLine}fold`);
892 |           return `Created fold from line ${startLine} to ${endLine}`;
893 |           
894 |         case 'open':
895 |           await nvim.input('zo');
896 |           return 'Opened fold at cursor';
897 |           
898 |         case 'close':
899 |           await nvim.input('zc');
900 |           return 'Closed fold at cursor';
901 |           
902 |         case 'toggle':
903 |           await nvim.input('za');
904 |           return 'Toggled fold at cursor';
905 |           
906 |         case 'openall':
907 |           await nvim.command('normal! zR');
908 |           return 'Opened all folds';
909 |           
910 |         case 'closeall':
911 |           await nvim.command('normal! zM');
912 |           return 'Closed all folds';
913 |           
914 |         case 'delete':
915 |           await nvim.input('zd');
916 |           return 'Deleted fold at cursor';
917 |           
918 |         default:
919 |           throw new NeovimValidationError(`Unknown fold action: ${action}`);
920 |       }
921 |     } catch (error) {
922 |       if (error instanceof NeovimValidationError) {
923 |         throw error;
924 |       }
925 |       console.error('Error managing fold:', error);
926 |       throw new NeovimCommandError(`fold ${action}`, error instanceof Error ? error.message : 'Unknown error');
927 |     }
928 |   }
929 | 
930 |   public async navigateJumpList(direction: string): Promise<string> {
931 |     try {
932 |       const nvim = await this.connect();
933 |       
934 |       switch (direction) {
935 |         case 'back':
936 |           await nvim.input('\x0f'); // Ctrl-O
937 |           return 'Jumped back in jump list';
938 |           
939 |         case 'forward':
940 |           await nvim.input('\x09'); // Ctrl-I (Tab)
941 |           return 'Jumped forward in jump list';
942 |           
943 |         case 'list':
944 |           await nvim.command('jumps');
945 |           // Get the output from the command
946 |           const output = await nvim.eval('execute("jumps")');
947 |           return `Jump list:\n${output}`;
948 |           
949 |         default:
950 |           throw new NeovimValidationError(`Unknown jump direction: ${direction}`);
951 |       }
952 |     } catch (error) {
953 |       if (error instanceof NeovimValidationError) {
954 |         throw error;
955 |       }
956 |       console.error('Error navigating jump list:', error);
957 |       throw new NeovimCommandError(`jump ${direction}`, error instanceof Error ? error.message : 'Unknown error');
958 |     }
959 |   }
960 | }
961 | 
```