🐼

Neovim内ターミナル前提でMCPサーバを作った(後編)

に公開

まず何を作ったか

Neovimの情報をMCPで取得できるようにするサーバをDenopsで作りました。

前提として、私は「ターミナルはNeovim内に閉じ込める」派です。
:terminal にこだわる理由や運用のクセについては、以前の記事で触れています
(例: NeovimのTerminalモードをもうちょっと使いやすくする)。

前編では、安定した入口として nvim-proxy を作った話を書きました。
https://zenn.dev/vim_jp/articles/27ffdef704e164
今回はその続きとして、この仕組みを使って作った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_buffers
  • nvim_current_buffer
  • nvim_cwd
  • nvim_current_selection
  • nvim_list_items
  • nvim_diagnostics
  • nvim_reload_buffer
  • nvim_get_buffer_content
  • nvim_save_buffer
  • nvim_open_file

基本は読み取り系ですが、バッファの保存やリロードなどの操作系も追加しました。

nvim_buffersdir / modifiedOnly / limit で絞り込みできます。
nvim_list_items はquickfixとloclistのどちらにも対応しています。
nvim_reload_buffernvim_save_buffer はバッファ内容に応じて安全側に処理をスキップします。

実装の置き場所

実装はdotfilesリポジトリの以下にあります。

  • nvim/denops/mcp/main.ts
  • nvim/denops/mcp/tools/*.ts
  • nvim/denops/mcp/util.ts
  • nvim/denops/mcp/README.md

GitHub上でも同じ構成で公開しています。
https://github.com/kyoh86/dotfiles/tree/a40a5d756848ddb75795b400f2144d74736cf0f0/nvim/denops/mcp

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 });
  },
});

登録時は/registerpid/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),
});

参照:
https://github.com/kyoh86/dotfiles/blob/a40a5d756848ddb75795b400f2144d74736cf0f0/nvim/denops/mcp/main.ts

まとめ

  • NeovimからMCPを生やすこと自体はDenopsで可能
  • ただし「入口の安定化」が必須
  • そのために前編の nvim-proxy が必要

Discussion