Skip to main content
Glama

mcp-neovim-server

#!/usr/bin/env node /** * This is an MCP server that connects to neovim. */ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { NeovimManager, NeovimConnectionError, NeovimCommandError, NeovimValidationError } from "./neovim.js"; import { z } from "zod"; const server = new McpServer( { name: "mcp-neovim-server", version: "0.5.2" } ); const neovimManager = NeovimManager.getInstance(); // Register resources server.resource( "session", new ResourceTemplate("nvim://session", { list: () => ({ resources: [{ uri: "nvim://session", mimeType: "text/plain", name: "Current neovim session", description: "Current neovim text editor session" }] }) }), async (uri) => { const bufferContents = await neovimManager.getBufferContents(); return { contents: [{ uri: uri.href, mimeType: "text/plain", text: Array.from(bufferContents.entries()) .map(([lineNum, lineText]) => `${lineNum}: ${lineText}`) .join('\n') }] }; } ); server.resource( "buffers", new ResourceTemplate("nvim://buffers", { list: () => ({ resources: [{ uri: "nvim://buffers", mimeType: "application/json", name: "Open Neovim buffers", description: "List of all open buffers in the current Neovim session" }] }) }), async (uri) => { const openBuffers = await neovimManager.getOpenBuffers(); return { contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(openBuffers, null, 2) }] }; } ); // Register tools with proper parameter schemas server.tool( "vim_buffer", "Get buffer contents with line numbers", { filename: z.string().optional().describe("Optional file name to view a specific buffer") }, async ({ filename }) => { try { const bufferContents = await neovimManager.getBufferContents(filename); return { content: [{ type: "text", text: Array.from(bufferContents.entries()) .map(([lineNum, lineText]) => `${lineNum}: ${lineText}`) .join('\n') }] }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : 'Error getting buffer contents' }] }; } } ); server.tool( "vim_command", "Execute Vim commands with optional shell command support", { command: z.string().describe("Vim command to execute (use ! prefix for shell commands if enabled)") }, async ({ command }) => { try { // Check if this is a shell command if (command.startsWith('!')) { const allowShellCommands = process.env.ALLOW_SHELL_COMMANDS === 'true'; if (!allowShellCommands) { return { content: [{ type: "text", text: "Shell command execution is disabled. Set ALLOW_SHELL_COMMANDS=true environment variable to enable shell commands." }] }; } } const result = await neovimManager.sendCommand(command); return { content: [{ type: "text", text: result }] }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : 'Error executing command' }] }; } } ); server.tool( "vim_status", "Get comprehensive Neovim status including cursor position, mode, marks, and registers", {}, async () => { try { const status = await neovimManager.getNeovimStatus(); return { content: [{ type: "text", text: JSON.stringify(status, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : 'Error getting Neovim status' }] }; } } ); server.tool( "vim_edit", "Edit buffer content using insert, replace, or replaceAll modes", { startLine: z.number().describe("The line number where editing should begin (1-indexed)"), mode: z.enum(["insert", "replace", "replaceAll"]).describe("Whether to insert new content, replace existing content, or replace entire buffer"), lines: z.string().describe("The text content to insert or use as replacement") }, async ({ startLine, mode, lines }) => { try { const result = await neovimManager.editLines(startLine, mode, lines); return { content: [{ type: "text", text: result }] }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : 'Error editing buffer' }] }; } } ); server.tool( "vim_window", "Manage Neovim windows: split, close, and navigate between windows", { command: z.enum(["split", "vsplit", "only", "close", "wincmd h", "wincmd j", "wincmd k", "wincmd l"]) .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") }, async ({ command }) => { try { const result = await neovimManager.manipulateWindow(command); return { content: [{ type: "text", text: result }] }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : 'Error manipulating window' }] }; } } ); server.tool( "vim_mark", "Set named marks at specific positions in the buffer", { mark: z.string().regex(/^[a-z]$/).describe("Single lowercase letter [a-z] to use as the mark name"), line: z.number().describe("The line number where the mark should be placed (1-indexed)"), column: z.number().describe("The column number where the mark should be placed (0-indexed)") }, async ({ mark, line, column }) => { try { const result = await neovimManager.setMark(mark, line, column); return { content: [{ type: "text", text: result }] }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : 'Error setting mark' }] }; } } ); server.tool( "vim_register", "Manage Neovim register contents", { register: z.string().regex(/^[a-z\"]$/).describe("Register name - a lowercase letter [a-z] or double-quote [\"] for the unnamed register"), content: z.string().describe("The text content to store in the specified register") }, async ({ register, content }) => { try { const result = await neovimManager.setRegister(register, content); return { content: [{ type: "text", text: result }] }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : 'Error setting register' }] }; } } ); server.tool( "vim_visual", "Create visual mode selections in the buffer", { startLine: z.number().describe("The starting line number for visual selection (1-indexed)"), startColumn: z.number().describe("The starting column number for visual selection (0-indexed)"), endLine: z.number().describe("The ending line number for visual selection (1-indexed)"), endColumn: z.number().describe("The ending column number for visual selection (0-indexed)") }, async ({ startLine, startColumn, endLine, endColumn }) => { try { const result = await neovimManager.visualSelect(startLine, startColumn, endLine, endColumn); return { content: [{ type: "text", text: result }] }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : 'Error creating visual selection' }] }; } } ); // New enhanced buffer management tools server.tool( "vim_buffer_switch", "Switch between buffers by name or number", { identifier: z.union([z.string(), z.number()]).describe("Buffer identifier - can be buffer number or filename/path") }, async ({ identifier }) => { try { const result = await neovimManager.switchBuffer(identifier); return { content: [{ type: "text", text: result }] }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : 'Error switching buffer' }] }; } } ); server.tool( "vim_buffer_save", "Save current buffer or save to specific filename", { filename: z.string().optional().describe("Optional filename to save buffer to (defaults to current buffer's filename)") }, async ({ filename }) => { try { const result = await neovimManager.saveBuffer(filename); return { content: [{ type: "text", text: result }] }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : 'Error saving buffer' }] }; } } ); server.tool( "vim_file_open", "Open files into new buffers", { filename: z.string().describe("Path to the file to open") }, async ({ filename }) => { try { const result = await neovimManager.openFile(filename); return { content: [{ type: "text", text: result }] }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : 'Error opening file' }] }; } } ); // New search and replace tools server.tool( "vim_search", "Search within current buffer with regex support and options", { pattern: z.string().describe("Search pattern (supports regex)"), ignoreCase: z.boolean().optional().describe("Whether to ignore case in search (default: false)"), wholeWord: z.boolean().optional().describe("Whether to match whole words only (default: false)") }, async ({ pattern, ignoreCase = false, wholeWord = false }) => { try { const result = await neovimManager.searchInBuffer(pattern, { ignoreCase, wholeWord }); return { content: [{ type: "text", text: result }] }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : 'Error searching in buffer' }] }; } } ); server.tool( "vim_search_replace", "Find and replace with global, case-insensitive, and confirm options", { pattern: z.string().describe("Search pattern (supports regex)"), replacement: z.string().describe("Replacement text"), global: z.boolean().optional().describe("Replace all occurrences in each line (default: false)"), ignoreCase: z.boolean().optional().describe("Whether to ignore case in search (default: false)"), confirm: z.boolean().optional().describe("Whether to confirm each replacement (default: false)") }, async ({ pattern, replacement, global = false, ignoreCase = false, confirm = false }) => { try { const result = await neovimManager.searchAndReplace(pattern, replacement, { global, ignoreCase, confirm }); return { content: [{ type: "text", text: result }] }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : 'Error in search and replace' }] }; } } ); server.tool( "vim_grep", "Project-wide search using vimgrep with quickfix list", { pattern: z.string().describe("Search pattern to grep for"), filePattern: z.string().optional().describe("File pattern to search in (default: **/* for all files)") }, async ({ pattern, filePattern = "**/*" }) => { try { const result = await neovimManager.grepInProject(pattern, filePattern); return { content: [{ type: "text", text: result }] }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : 'Error in grep search' }] }; } } ); // Health check tool server.tool( "vim_health", "Check Neovim connection health", {}, async () => { const isHealthy = await neovimManager.healthCheck(); return { content: [{ type: "text", text: isHealthy ? "Neovim connection is healthy" : "Neovim connection failed" }] }; } ); // Macro management tool server.tool( "vim_macro", "Record, stop, and play Neovim macros", { action: z.enum(["record", "stop", "play"]).describe("Action to perform with macros"), register: z.string().optional().describe("Register to record/play macro (a-z, required for record/play)"), count: z.number().optional().describe("Number of times to play macro (default: 1)") }, async ({ action, register, count = 1 }) => { try { const result = await neovimManager.manageMacro(action, register, count); return { content: [{ type: "text", text: result }] }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : 'Error managing macro' }] }; } } ); // Tab management tool server.tool( "vim_tab", "Manage Neovim tabs: create, close, and navigate between tabs", { action: z.enum(["new", "close", "next", "prev", "first", "last", "list"]).describe("Tab action to perform"), filename: z.string().optional().describe("Filename for new tab (optional)") }, async ({ action, filename }) => { try { const result = await neovimManager.manageTab(action, filename); return { content: [{ type: "text", text: result }] }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : 'Error managing tab' }] }; } } ); // Code folding tool server.tool( "vim_fold", "Manage code folding: create, open, close, and toggle folds", { action: z.enum(["create", "open", "close", "toggle", "openall", "closeall", "delete"]).describe("Folding action to perform"), startLine: z.number().optional().describe("Start line for creating fold (required for create)"), endLine: z.number().optional().describe("End line for creating fold (required for create)") }, async ({ action, startLine, endLine }) => { try { const result = await neovimManager.manageFold(action, startLine, endLine); return { content: [{ type: "text", text: result }] }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : 'Error managing fold' }] }; } } ); // Jump list navigation tool server.tool( "vim_jump", "Navigate Neovim jump list: go back, forward, or list jumps", { direction: z.enum(["back", "forward", "list"]).describe("Jump direction or list jumps") }, async ({ direction }) => { try { const result = await neovimManager.navigateJumpList(direction); return { content: [{ type: "text", text: result }] }; } catch (error) { return { content: [{ type: "text", text: error instanceof Error ? error.message : 'Error navigating jump list' }] }; } } ); // Register a sample prompt for Neovim workflow assistance server.prompt( "neovim_workflow", "Get help with common Neovim workflows and editing tasks", { task: z.enum(["editing", "navigation", "search", "buffers", "windows", "macros"]).describe("Type of Neovim task you need help with") }, async ({ task }) => { const workflows = { 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", 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", 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", 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", 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", 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" }; return { messages: [ { role: "assistant", content: { type: "text", text: workflows[task] || "Unknown task type. Available tasks: editing, navigation, search, buffers, windows, macros" } } ] }; } ); /** * Start the server using stdio transport. * This allows the server to communicate via standard input/output streams. */ async function main() { const transport = new StdioServerTransport(); await server.connect(transport); } main().catch((error) => { console.error("Server error:", error); process.exit(1); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bigcodegen/mcp-neovim-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server