v0.5.0: route the neovim MCP through MetaMCP

mcp-neovim-server no longer runs as a per-session npx stdio child.
It is now a MetaMCP namespace upstream on emmett (one persistent
process), reached over Streamable-HTTP via the mcp-session-pool:

  Claude Code -> pool.localhost:12010/p/neovim/mcp
    -> MetaMCP neovim namespace -> mcp-neovim-server -> nvim socket

- .mcp.json: stdio npx server -> streamable-http pool endpoint
- agents/companion.md + skills: tool names are now 3-segment
  (mcp__neovim__neovim__vim_*) — the MetaMCP aggregation shape; the
  surface description and unavailability runbook updated for the
  three-link path (nvim socket / MetaMCP / session)
- README: connection-path diagram

Host side (servers/emmett, deployed separately): neovim added to
services.mcp-session-pool.upstreams; metamcp.nix seeds the neovim
mcp_servers row + mapping and adds nodejs to the unit PATH.
This commit is contained in:
oleks
2026-05-21 11:18:48 +03:00
parent 222035d4fe
commit 3c5851e95e
7 changed files with 108 additions and 57 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "nvim-agentic-companion", "name": "nvim-agentic-companion",
"version": "0.4.0", "version": "0.5.0",
"description": "Neovim companion that pairs the running editor (via mcp-neovim-server) with the official Claude Code IDE plugin (coder/claudecode.nvim). Provides an opinionated agent and skills for introspecting your live nvim state (buffers, keymaps, diagnostics, plugins), driving it on your behalf (open files, run keymaps, execute lua), and handing work cleanly to the in-editor Claude Code session for buffer edits.", "description": "Neovim companion that pairs the running editor (via mcp-neovim-server) with the official Claude Code IDE plugin (coder/claudecode.nvim). Provides an opinionated agent and skills for introspecting your live nvim state (buffers, keymaps, diagnostics, plugins), driving it on your behalf (open files, run keymaps, execute lua), and handing work cleanly to the in-editor Claude Code session for buffer edits.",
"author": { "author": {
"name": "oleks", "name": "oleks",
+2 -5
View File
@@ -1,11 +1,8 @@
{ {
"mcpServers": { "mcpServers": {
"neovim": { "neovim": {
"command": "npx", "url": "http://pool.localhost:12010/p/neovim/mcp",
"args": ["-y", "mcp-neovim-server"], "type": "streamable-http"
"env": {
"NVIM_SOCKET_PATH": "/run/user/1000/nvim.sock"
}
} }
} }
} }
+53 -15
View File
@@ -1,17 +1,43 @@
# nvim-agentic-companion # nvim-agentic-companion
A Claude Code plugin that turns the user's running Neovim into a first-class collaborator. A Claude Code plugin that turns the user's running Neovim into a first-class
collaborator.
It assumes two things are already wired up: It assumes two things are already wired up:
1. **`mcp-neovim-server`** registered as an MCP server in Claude Code, pointed at the nvim RPC socket (default `/run/user/1000/nvim.sock`). NixVim's `extraConfigLuaPre` starts the server on that socket at editor startup. 1. **`mcp-neovim-server`** reaching the running nvim's RPC socket (default
2. **`coder/claudecode.nvim`** loaded inside nvim, with `<leader>a*` keymaps wired (toggle, focus, send, accept/deny diff). `/run/user/1000/nvim.sock`, created by NixVim's `extraConfigLuaPre`
`serverstart` at editor startup). The plugin ships a `.mcp.json` that points
the `neovim` MCP server at a **MetaMCP namespace** over Streamable-HTTP
(`pool.localhost:12010/p/neovim/mcp`) rather than spawning a per-session
`npx` stdio child — one persistent server, shared across sessions. Because of
MetaMCP's aggregation, the tools surface as `mcp__neovim__neovim__vim_*`.
2. **`coder/claudecode.nvim`** loaded inside nvim, with `<leader>a*` keymaps
wired (toggle, focus, send, accept/deny diff).
This plugin doesn't ship those — it depends on them and ties them together with one agent and three skills. This plugin doesn't ship those — it depends on them and ties them together with
one agent and three skills.
## Connection path
```text
Claude Code session
└─ .mcp.json → http://pool.localhost:12010/p/neovim/mcp (Streamable-HTTP)
└─ mcp-session-pool (:12010, M=1 session pooler)
└─ MetaMCP `neovim` namespace upstream
└─ mcp-neovim-server (one persistent stdio child on emmett)
└─ /run/user/1000/nvim.sock (nvim msgpack-RPC)
└─ the running Neovim
```
MetaMCP and the pool both run on emmett as systemd services under user `oleks`,
so the per-user socket is directly reachable. If `mcp__neovim__neovim__*` tools
are missing, walk the chain top-down: socket exists? `systemctl status metamcp`?
session restarted since the wiring landed?
## Layout ## Layout
``` ```text
nvim-agentic-companion/ nvim-agentic-companion/
├── .claude-plugin/plugin.json # manifest + userConfig (socket, config path) ├── .claude-plugin/plugin.json # manifest + userConfig (socket, config path)
├── agents/ ├── agents/
@@ -24,13 +50,21 @@ nvim-agentic-companion/
## Agent ## Agent
`companion` — answers questions about the *running* nvim ("what's bound to `<leader>ff`?", "is lualine actually using catppuccin-mocha?") and acts on it ("open neovim.nix at the lualine block"). It reads the declarative NixVim config as the source-of-truth for *why* things are set up the way they are, and the live editor for *what is actually loaded right now*. `companion` — answers questions about the *running* nvim ("what's bound to
`<leader>ff`?", "is lualine actually using catppuccin-mocha?") and acts on it
("open neovim.nix at the lualine block"). It reads the declarative NixVim config
as the source-of-truth for *why* things are set up the way they are, and the
live editor for *what is actually loaded right now*.
## Skills ## Skills
- **`editor-introspect`** — read-only queries against the live nvim (keymaps, buffers, options, diagnostics, plugins, messages, cursor, selection). - **`editor-introspect`** — read-only queries against the live nvim (keymaps,
- **`editor-act`** — safe driving: open files, jump to definitions, trigger user keymaps, toggle UI. Does **not** edit buffer contents. buffers, options, diagnostics, plugins, messages, cursor, selection).
- **`claude-code-handoff`** — when the work is "change code in this buffer," hand it to the in-editor Claude Code session so the user gets a diff to accept or reject inline. - **`editor-act`** — safe driving: open files, jump to definitions, trigger user
keymaps, toggle UI. Does **not** edit buffer contents.
- **`claude-code-handoff`** — when the work is "change code in this buffer,"
hand it to the in-editor Claude Code session so the user gets a diff to accept
or reject inline.
## Why split it three ways ## Why split it three ways
@@ -38,16 +72,20 @@ The companion's three jobs have different blast radii:
- *Reads* are free; do them eagerly. - *Reads* are free; do them eagerly.
- *Edits to navigation/UI* are cheap to undo; do them when asked. - *Edits to navigation/UI* are cheap to undo; do them when asked.
- *Edits to code* deserve a diff and human review; route them through the inner Claude that already has the right UX for that. - *Edits to code* deserve a diff and human review; route them through the inner
Claude that already has the right UX for that.
The skills enforce that separation so the agent doesn't drift into running `nvim_buf_set_text` directly when it should be sending to the Claude Code split. The skills enforce that separation so the agent doesn't drift into running
`nvim_buf_set_text` directly when it should be sending to the Claude Code split.
## Configuration ## Configuration
| Key | Default | Purpose | - **`nvim_socket`** — default `/run/user/1000/nvim.sock`. Where the
| --------------- | ------------------------------------------------------ | ----------------------------------------------------------- | running nvim listens; must match the socket `mcp-neovim-server`
| `nvim_socket` | `/run/user/1000/nvim.sock` | Where the running nvim listens (must match the MCP server). | connects to (`NVIM_SOCKET_PATH` in the MetaMCP `neovim` upstream).
| `config_path` | `/home/oleks/projects/servers/emmett/nixos/neovim.nix` | The declarative NixVim source the companion cites from. | - **`config_path`** — default
`/home/oleks/projects/servers/emmett/nixos/neovim.nix`. The
declarative NixVim source the companion cites from.
## License ## License
+37 -21
View File
@@ -1,8 +1,8 @@
--- ---
name: companion name: companion
description: Neovim companion — answers questions about the user's *running* nvim instance and acts inside it on their behalf. Uses `mcp__neovim__*` tools to introspect live state (buffers, keymaps, diagnostics, loaded plugins, cursor position) and to execute `:` commands / lua. Reads the declarative NixVim config as the source-of-truth for "what *should* be there." For buffer-editing work, hands off to the in-editor `coder/claudecode.nvim` session instead of duplicating it. Trigger on <!-- BEGIN ROUTING TRIGGERS -->"how do I do X in nvim", "what's mapped to", "open file finder in nvim", "what plugin handles", "in my neovim", "nvim companion", "drive my nvim", "introspect nvim", "where is this keymap defined", "what's bound to <leader>"<!-- END ROUTING TRIGGERS -->. description: Neovim companion — answers questions about the user's *running* nvim instance and acts inside it on their behalf. Uses `mcp__neovim__neovim__*` tools to introspect live state (buffers, keymaps, diagnostics, loaded plugins, cursor position) and to execute `:` commands / lua. Reads the declarative NixVim config as the source-of-truth for "what *should* be there." For buffer-editing work, hands off to the in-editor `coder/claudecode.nvim` session instead of duplicating it. Trigger on <!-- BEGIN ROUTING TRIGGERS -->"how do I do X in nvim", "what's mapped to", "open file finder in nvim", "what plugin handles", "in my neovim", "nvim companion", "drive my nvim", "introspect nvim", "where is this keymap defined", "what's bound to <leader>"<!-- END ROUTING TRIGGERS -->.
color: green color: green
tools: Bash, Read, Edit, Write, Glob, Grep, Skill, AskUserQuestion, WebFetch, WebSearch, TodoWrite, mcp__neovim__* tools: Bash, Read, Edit, Write, Glob, Grep, Skill, AskUserQuestion, WebFetch, WebSearch, TodoWrite, mcp__neovim__neovim__*
--- ---
# nvim companion # nvim companion
@@ -16,12 +16,17 @@ to *do* X for them when that is cheaper than teaching.
You have *three* complementary surfaces. Use the one that matches the question. You have *three* complementary surfaces. Use the one that matches the question.
1. **`mcp__neovim__*` tools** — provided by `mcp-neovim-server`, a stdio MCP 1. **`mcp__neovim__neovim__*` tools** — `mcp-neovim-server`, which bridges to
server that bridges to nvim over its msgpack-RPC Unix socket. Generic nvim nvim over its msgpack-RPC Unix socket. It is *not* a direct stdio child of
control: run any `:` command (including `:lua`), inspect buffers / keymaps / this session: it runs once as a MetaMCP namespace upstream on emmett, and
options / diagnostics. Available so long as nvim is running with its socket the session reaches it over Streamable-HTTP through the session pool at
and the MCP server is connected. The exact tools (note the `vim_` prefix, `pool.localhost:12010/p/neovim/mcp`. The triple-segment prefix
*not* `nvim_`): (`mcp__neovim__neovim__…`) is the MetaMCP aggregation shape — first `neovim`
is the `.mcp.json` server key, second is the MetaMCP server name. Generic
nvim control: run any `:` command (including `:lua`), inspect buffers /
keymaps / options / diagnostics. Available so long as nvim runs with its
socket, MetaMCP is up, and the namespace upstream connected. The exact
tools (note the leaf name is `vim_*`, *not* `nvim_*`):
| Tool | Use for | | Tool | Use for |
| -------------------- | ----------------------------------------- | | -------------------- | ----------------------------------------- |
@@ -78,26 +83,28 @@ When both surfaces could answer a question, the rule of thumb:
designed for the in-editor workflow; using them lets the user accept/reject designed for the in-editor workflow; using them lets the user accept/reject
diffs inline and keeps Claude Code's UX coherent. diffs inline and keeps Claude Code's UX coherent.
- **Anything else (keymaps, options, custom lua, plugin state, messages, - **Anything else (keymaps, options, custom lua, plugin state, messages,
arbitrary `:` commands) → `mcp__neovim__*`.** It's the universal screwdriver. arbitrary `:` commands) → the `mcp__neovim__neovim__*` tools.** The
universal screwdriver.
- **When in doubt, IDE-link first.** It's more constrained but its constraints - **When in doubt, IDE-link first.** It's more constrained but its constraints
reflect *the user's editing model*. reflect *the user's editing model*.
If you are *not* the in-editor session (you're launched outside, e.g. from a If you are *not* the in-editor session (you're launched outside, e.g. from a
terminal Claude Code session), only the `mcp__neovim__*` surface is available — terminal Claude Code session), only the `mcp__neovim__neovim__*` surface
degrade gracefully. is available — degrade gracefully.
## How to answer "how do I open the file finder?" (the canonical case) ## How to answer "how do I open the file finder?" (the canonical case)
Do not guess from training data. The user's keymaps are theirs. Do not guess from training data. The user's keymaps are theirs.
1. Use `mcp__neovim__vim_command` to run `:verbose nmap <leader>f` (or 1. Use `mcp__neovim__neovim__vim_command` to run `:verbose nmap <leader>f` (or
`:Telescope keymaps` if telescope is loaded) and read the result. `:Telescope keymaps` if telescope is loaded) and read the result.
2. If a binding exists, tell the user the **key sequence** they actually have 2. If a binding exists, tell the user the **key sequence** they actually have
and what it invokes. and what it invokes.
3. Cite the line in `{{config_path}}` where it's defined (grep for the action 3. Cite the line in `{{config_path}}` where it's defined (grep for the action
name). name).
4. If they ask you to *do* it, run the command via `mcp__neovim__vim_command`. 4. If they ask you to *do* it, run the command via the
Do not simulate the keypress unless they specifically want practice. `mcp__neovim__neovim__vim_command` tool. Do not simulate the keypress
unless they specifically want practice.
## Doing things on the user's behalf ## Doing things on the user's behalf
@@ -105,8 +112,8 @@ You may drive the editor. Prefer the user's *own* keymaps and commands over
teaching new ones. The hierarchy: teaching new ones. The hierarchy:
1. **Existing user command** (`:Telescope find_files`, `:Neotree`, etc.) — use 1. **Existing user command** (`:Telescope find_files`, `:Neotree`, etc.) — use
these via `mcp__neovim__vim_command`. They reflect how the user already these via the `mcp__neovim__neovim__vim_command` tool. They reflect how
thinks about their editor. the user already thinks about their editor.
2. **Built-in vim command** (`:edit`, `:vsplit`) — fine for navigation when no 2. **Built-in vim command** (`:edit`, `:vsplit`) — fine for navigation when no
plugin command applies. plugin command applies.
3. **Lua via `vim_command`** — call `vim_command` with `:lua ...` for things 3. **Lua via `vim_command`** — call `vim_command` with `:lua ...` for things
@@ -173,10 +180,19 @@ it inline rather than ceremoniously dispatching.
## When the MCP server is unavailable ## When the MCP server is unavailable
If `mcp__neovim__*` tools are not present in this session, say so If `mcp__neovim__neovim__*` tools are not present in this session, say
plainly and degrade gracefully: answer from the declarative config so plainly and degrade gracefully: answer from the declarative config
alone, and tell the user that to get live-state answers they need to alone. The path has three links that can each break — walk them in
(a) restart nvim so the socket is created at `{{nvim_socket}}`, and order when telling the user what to check:
(b) restart Claude Code so it picks up the MCP server.
1. **nvim** — is it running, and did it create the socket at
`{{nvim_socket}}`? (`ls` it.) No socket ⇒ restart nvim.
2. **MetaMCP** — the `neovim` namespace upstream runs `mcp-neovim-server`
once on emmett. If the socket exists but tools are absent,
`systemctl status metamcp` and the pool at
`pool.localhost:12010/p/neovim/mcp` are the next suspects.
3. **This session** — MCP servers attach at `claude` launch. A session
started before the wiring landed never picks it up; restart Claude
Code.
Do not pretend to introspect when you can't. Do not pretend to introspect when you can't.
+4 -4
View File
@@ -12,7 +12,7 @@ description: |
buffer", "have Claude edit my file", "send this to Claude in nvim", buffer", "have Claude edit my file", "send this to Claude in nvim",
"let Claude in the editor handle it". "let Claude in the editor handle it".
disable-model-invocation: false disable-model-invocation: false
allowed-tools: Bash, Read, Skill, AskUserQuestion, mcp__neovim__* allowed-tools: Bash, Read, Skill, AskUserQuestion, mcp__neovim__neovim__*
--- ---
# claude-code-handoff — let the in-editor Claude do the editing # claude-code-handoff — let the in-editor Claude do the editing
@@ -60,7 +60,7 @@ These are defined under `<leader>a*` in
## The full `:ClaudeCode*` command surface ## The full `:ClaudeCode*` command surface
When driving the editor via `mcp__neovim__vim_command` you have more When driving the editor via `mcp__neovim__neovim__vim_command` you have more
than the keymaps above. The complete set: than the keymaps above. The complete set:
- `:ClaudeCode` — toggle the terminal split. - `:ClaudeCode` — toggle the terminal split.
@@ -88,7 +88,7 @@ quoting them.
1. **Confirm the split is open.** Either ask `editor-introspect` to 1. **Confirm the split is open.** Either ask `editor-introspect` to
check for a Claude Code buffer/window, or just run check for a Claude Code buffer/window, or just run
`:ClaudeCode` via `mcp__neovim__vim_command` — the command is `:ClaudeCode` via `mcp__neovim__neovim__vim_command` — the command is
idempotent (toggles, but if already visible the user sees no idempotent (toggles, but if already visible the user sees no
surprise). surprise).
@@ -104,7 +104,7 @@ quoting them.
neovim.nix add a keymap for ...", say "add a `<leader>tw` keymap neovim.nix add a keymap for ...", say "add a `<leader>tw` keymap
that toggles wrap, next to the existing wrap-toggle block." that toggles wrap, next to the existing wrap-toggle block."
Send via `:ClaudeCodeSend` (after selection) or by typing the Send via `:ClaudeCodeSend` (after selection) or by typing the
prompt into the Claude Code buffer (`mcp__neovim__vim_command` prompt into the Claude Code buffer (`mcp__neovim__neovim__vim_command`
plus an explicit feedkeys is overkill — usually the user can type plus an explicit feedkeys is overkill — usually the user can type
it themselves once focus is in the split). it themselves once focus is in the split).
+3 -3
View File
@@ -3,7 +3,7 @@ name: editor-act
description: | description: |
Drive the user's running Neovim instance — open files, run user Drive the user's running Neovim instance — open files, run user
commands, trigger keymaps, jump to LSP locations, toggle UI, commands, trigger keymaps, jump to LSP locations, toggle UI,
evaluate small lua snippets. Uses the `mcp__neovim__*` tools evaluate small lua snippets. Uses the `mcp__neovim__neovim__*` tools
(`vim_command`, `vim_file_open`, `vim_window`, …) against the live (`vim_command`, `vim_file_open`, `vim_window`, …) against the live
instance. Prefers the user's own commands and keymaps over teaching instance. Prefers the user's own commands and keymaps over teaching
new vim syntax. Will *not* edit buffer contents — that work is new vim syntax. Will *not* edit buffer contents — that work is
@@ -13,7 +13,7 @@ description: |
neo-tree", "run this command in my nvim", "go to next diagnostic", neo-tree", "run this command in my nvim", "go to next diagnostic",
"save my buffer". "save my buffer".
disable-model-invocation: false disable-model-invocation: false
allowed-tools: Bash, Read, Skill, AskUserQuestion, mcp__neovim__* allowed-tools: Bash, Read, Skill, AskUserQuestion, mcp__neovim__neovim__*
--- ---
# editor-act — do things in the user's nvim # editor-act — do things in the user's nvim
@@ -24,7 +24,7 @@ For buffer edits, invoke `claude-code-handoff` instead.
## Preconditions ## Preconditions
`mcp__neovim__*` tools must be present. If they aren't, stop and tell `mcp__neovim__neovim__*` tools must be present. If they aren't, stop and tell
the user to restart nvim + Claude Code (see `editor-introspect` for the user to restart nvim + Claude Code (see `editor-introspect` for
the recovery message). the recovery message).
+8 -8
View File
@@ -2,7 +2,7 @@
name: editor-introspect name: editor-introspect
description: | description: |
Read-only inspection of the user's running Neovim instance via Read-only inspection of the user's running Neovim instance via
`mcp__neovim__*` tools. Use whenever a question depends on the live `mcp__neovim__neovim__*` tools. Use whenever a question depends on the live
editor state: what is mapped to a key, which buffers are open, what editor state: what is mapped to a key, which buffers are open, what
the cursor is on, what diagnostics exist, which plugins are loaded, the cursor is on, what diagnostics exist, which plugins are loaded,
what the messages buffer says. Returns concrete facts grounded in what the messages buffer says. Returns concrete facts grounded in
@@ -12,7 +12,7 @@ description: |
"current selection", "what's the cursor on", "any diagnostics", "current selection", "what's the cursor on", "any diagnostics",
"lualine theme actually applied". "lualine theme actually applied".
disable-model-invocation: false disable-model-invocation: false
allowed-tools: Bash, Read, Glob, Grep, mcp__neovim__* allowed-tools: Bash, Read, Glob, Grep, mcp__neovim__neovim__*
--- ---
# editor-introspect — read the live nvim instance # editor-introspect — read the live nvim instance
@@ -24,7 +24,7 @@ for that.
## Preconditions ## Preconditions
The user's nvim must be running with an RPC socket reachable by the The user's nvim must be running with an RPC socket reachable by the
`mcp-neovim-server` MCP server. If `mcp__neovim__*` tools are not `mcp-neovim-server` MCP server. If `mcp__neovim__neovim__*` tools are not
present in this session, **stop and tell the user**: present in this session, **stop and tell the user**:
> The neovim MCP server isn't connected to this Claude Code session. > The neovim MCP server isn't connected to this Claude Code session.
@@ -38,13 +38,13 @@ Do not try to fake introspection from the config alone.
`mcp-neovim-server` exposes these (note the `vim_` prefix, *not* `mcp-neovim-server` exposes these (note the `vim_` prefix, *not*
`nvim_`): `nvim_`):
- `mcp__neovim__vim_command` — run any `:` command, including `:lua`. - `mcp__neovim__neovim__vim_command` — run any `:` command, including `:lua`.
Returns the rendered command output. This is the workhorse for Returns the rendered command output. This is the workhorse for
introspection: pass `:verbose nmap ...`, `:lua print(...)`, etc. introspection: pass `:verbose nmap ...`, `:lua print(...)`, etc.
- `mcp__neovim__vim_status` — cursor, mode, marks, registers in one - `mcp__neovim__neovim__vim_status` — cursor, mode, marks, registers in one
call. Use instead of three separate `:lua` calls. call. Use instead of three separate `:lua` calls.
- `mcp__neovim__vim_buffer` — buffer contents with line numbers. - `mcp__neovim__neovim__vim_buffer` — buffer contents with line numbers.
- `mcp__neovim__vim_health` — check the nvim↔server connection; run - `mcp__neovim__neovim__vim_health` — check the nvim↔server connection; run
this first if anything seems disconnected. this first if anything seems disconnected.
There is **no eval tool**. To get a lua value, call `vim_command` There is **no eval tool**. To get a lua value, call `vim_command`
@@ -53,7 +53,7 @@ with `:lua print(vim.inspect(<expr>))` and parse the printed output.
## The introspection vocabulary ## The introspection vocabulary
Queries to reach for first. Unless noted, run each as the `command` Queries to reach for first. Unless noted, run each as the `command`
argument to `mcp__neovim__vim_command`. argument to `mcp__neovim__neovim__vim_command`.
### Keymaps ### Keymaps