mcp-neovim-server
by bigcodegen
Verified
#!/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 } from "./neovim.js";
import { z } from "zod";
const server = new McpServer(
{
name: "mcp-neovim-server",
version: "0.4.1"
}
);
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",
{ filename: z.string().optional().describe("Optional file name to view a specific buffer") },
async ({ filename }) => {
const bufferContents = await neovimManager.getBufferContents();
return {
content: [{
type: "text",
text: Array.from(bufferContents.entries())
.map(([lineNum, lineText]) => `${lineNum}: ${lineText}`)
.join('\n')
}]
};
}
);
server.tool(
"vim_command",
{ command: z.string().describe("Vim command to execute (use ! prefix for shell commands if enabled)") },
async ({ command }) => {
console.error(`Executing command: ${command}`);
// 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
}]
};
}
);
server.tool(
"vim_status",
{ filename: z.string().optional().describe("Optional file name to get status for a specific buffer") },
async () => {
const status = await neovimManager.getNeovimStatus();
return {
content: [{
type: "text",
text: JSON.stringify(status)
}]
};
}
);
server.tool(
"vim_edit",
{
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 }) => {
console.error(`Editing lines: ${startLine}, ${mode}, ${lines}`);
const result = await neovimManager.editLines(startLine, mode, lines);
return {
content: [{
type: "text",
text: result
}]
};
}
);
server.tool(
"vim_window",
{
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 }) => {
const result = await neovimManager.manipulateWindow(command);
return {
content: [{
type: "text",
text: result
}]
};
}
);
server.tool(
"vim_mark",
{
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 }) => {
const result = await neovimManager.setMark(mark, line, column);
return {
content: [{
type: "text",
text: result
}]
};
}
);
server.tool(
"vim_register",
{
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 }) => {
const result = await neovimManager.setRegister(register, content);
return {
content: [{
type: "text",
text: result
}]
};
}
);
server.tool(
"vim_visual",
{
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 }) => {
const result = await neovimManager.visualSelect(startLine, startColumn, endLine, endColumn);
return {
content: [{
type: "text",
text: result
}]
};
}
);
// Register an empty prompts list since we don't support any prompts. Clients still ask.
server.prompt("empty", {}, () => ({
messages: []
}));
/**
* 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);
});