🐼
Neovim内ターミナル前提でMCPサーバを作った(後編)
まず何を作ったか
Neovimの情報をMCPで取得できるようにするサーバをDenopsで作りました。
前提として、私は「ターミナルはNeovim内に閉じ込める」派です。
:terminal にこだわる理由や運用のクセについては、以前の記事で触れています
(例: NeovimのTerminalモードをもうちょっと使いやすくする)。
前編では、安定した入口として nvim-proxy を作った話を書きました。
今回はその続きとして、この仕組みを使って作ったMCPサーバの実装をまとめます。
対象読者
- Neovimの
:terminal内で作業することが多い人 - Neovimの状態を外部から読みたい人
- MCPをローカルで試してみたい人
何ができるようになったか
MCP経由で以下の情報が取れるようになりました。
- バッファ一覧
- カレントバッファとカーソル位置
- Neovimのcwd
- visual selection
- quickfix / loclist
- diagnostics
- バッファのリロード
- バッファ内容の取得
- バッファの保存
- ファイルをバッファとして開く
実装のポイント
Denops で MCP サーバを立てる
-
nvim/denops/mcp/main.tsにMCPサーバを実装 -
McpServer+WebStandardStreamableHTTPServerTransportを使用 -
/mcpをハンドリングするHTTPサーバとして起動 - ポートはランダム
この時点では「ランダムポートをどう扱うか」が課題として残ります。
実際のツール定義
以下のツールを作っています(例)。
nvim_buffersnvim_current_buffernvim_cwdnvim_current_selectionnvim_list_itemsnvim_diagnosticsnvim_reload_buffernvim_get_buffer_contentnvim_save_buffernvim_open_file
基本は読み取り系ですが、バッファの保存やリロードなどの操作系も追加しました。
nvim_buffers は dir / modifiedOnly / limit で絞り込みできます。
nvim_list_items はquickfixとloclistのどちらにも対応しています。
nvim_reload_buffer や nvim_save_buffer はバッファ内容に応じて安全側に処理をスキップします。
実装の置き場所
実装はdotfilesリポジトリの以下にあります。
nvim/denops/mcp/main.tsnvim/denops/mcp/tools/*.tsnvim/denops/mcp/util.tsnvim/denops/mcp/README.md
GitHub上でも同じ構成で公開しています。
MCP で呼び出す例
nvim_current_buffer を呼び出すと、こういうJSONが返ります。
{
"name": "term://~/Projects/github.com/kyoh86/dotfiles//...",
"bufnr": 4,
"modified": false,
"cursor": { "line": 100, "col": 3 },
"cwd": "/home/kyoh86/Projects/github.com/kyoh86/dotfiles"
}
入口の安定化
MCPサーバ単体ではランダムポート問題が残るため、前編の nvim-proxy を介してアクセスするようにしています。
Codex側の設定例:
[mcp_servers.nvim_proxy]
url = "http://127.0.0.1:37125/mcp"
env_http_headers = { "X-Nvim-Pid" = "NVIM_PID" }
MCPサーバーの起動時にランダムポートをnvim-proxyに登録しています。
const { finished } = Deno.serve({
hostname: host,
port,
handler,
onListen: async ({ port }) => {
await registerToProxy(denops, { port });
},
});
登録時は/registerにpidと/mcpの対応を送ります。
const payload = {
pid: options.pid,
proxy_path: "/mcp",
reverse_port: options.port,
reverse_path: "/mcp",
};
await fetch(registerUrl, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(payload),
});
参照:
まとめ
- NeovimからMCPを生やすこと自体はDenopsで可能
- ただし「入口の安定化」が必須
- そのために前編の
nvim-proxyが必要
Discussion