hyprpilot-nvim-mcp 1.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hyprpilot_nvim_mcp-1.1.2/.gitignore +65 -0
- hyprpilot_nvim_mcp-1.1.2/CLAUDE.md +349 -0
- hyprpilot_nvim_mcp-1.1.2/LICENSE +21 -0
- hyprpilot_nvim_mcp-1.1.2/PKG-INFO +66 -0
- hyprpilot_nvim_mcp-1.1.2/README.md +50 -0
- hyprpilot_nvim_mcp-1.1.2/Taskfile.yml +39 -0
- hyprpilot_nvim_mcp-1.1.2/hyprpilot_nvim_mcp/__init__.py +10 -0
- hyprpilot_nvim_mcp-1.1.2/hyprpilot_nvim_mcp/cli.py +114 -0
- hyprpilot_nvim_mcp-1.1.2/hyprpilot_nvim_mcp/dispatcher.py +86 -0
- hyprpilot_nvim_mcp-1.1.2/hyprpilot_nvim_mcp/log.py +51 -0
- hyprpilot_nvim_mcp-1.1.2/hyprpilot_nvim_mcp/nvim.py +103 -0
- hyprpilot_nvim_mcp-1.1.2/hyprpilot_nvim_mcp/server.py +10 -0
- hyprpilot_nvim_mcp-1.1.2/hyprpilot_nvim_mcp/tools/__init__.py +1 -0
- hyprpilot_nvim_mcp-1.1.2/hyprpilot_nvim_mcp/tools/healthcheck.py +48 -0
- hyprpilot_nvim_mcp-1.1.2/hyprpilot_nvim_mcp/tools/reload.py +71 -0
- hyprpilot_nvim_mcp-1.1.2/pyproject.toml +56 -0
- hyprpilot_nvim_mcp-1.1.2/tests/__init__.py +0 -0
- hyprpilot_nvim_mcp-1.1.2/tests/conftest.py +51 -0
- hyprpilot_nvim_mcp-1.1.2/tests/test_dispatcher_integration.py +235 -0
- hyprpilot_nvim_mcp-1.1.2/tests/test_smoke.py +115 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Compiled Lua sources
|
|
2
|
+
luac.out
|
|
3
|
+
|
|
4
|
+
# luarocks build files
|
|
5
|
+
*.src.rock
|
|
6
|
+
*.zip
|
|
7
|
+
*.tar.gz
|
|
8
|
+
|
|
9
|
+
# Object files
|
|
10
|
+
*.o
|
|
11
|
+
*.os
|
|
12
|
+
*.ko
|
|
13
|
+
*.obj
|
|
14
|
+
*.elf
|
|
15
|
+
|
|
16
|
+
# Precompiled Headers
|
|
17
|
+
*.gch
|
|
18
|
+
*.pch
|
|
19
|
+
|
|
20
|
+
# Libraries
|
|
21
|
+
*.lib
|
|
22
|
+
*.a
|
|
23
|
+
*.la
|
|
24
|
+
*.lo
|
|
25
|
+
*.def
|
|
26
|
+
*.exp
|
|
27
|
+
|
|
28
|
+
# Shared objects (inc. Windows DLLs)
|
|
29
|
+
*.dll
|
|
30
|
+
*.so
|
|
31
|
+
*.so.*
|
|
32
|
+
*.dylib
|
|
33
|
+
|
|
34
|
+
# Executables
|
|
35
|
+
*.exe
|
|
36
|
+
*.out
|
|
37
|
+
*.app
|
|
38
|
+
*.i*86
|
|
39
|
+
*.x86_64
|
|
40
|
+
*.hex
|
|
41
|
+
/.task
|
|
42
|
+
|
|
43
|
+
# Python (pkg/)
|
|
44
|
+
__pycache__/
|
|
45
|
+
*.py[cod]
|
|
46
|
+
*$py.class
|
|
47
|
+
build/
|
|
48
|
+
dist/
|
|
49
|
+
wheels/
|
|
50
|
+
*.egg-info/
|
|
51
|
+
*.egg
|
|
52
|
+
.venv/
|
|
53
|
+
venv/
|
|
54
|
+
env/
|
|
55
|
+
.mypy_cache/
|
|
56
|
+
.ruff_cache/
|
|
57
|
+
.pytest_cache/
|
|
58
|
+
.coverage
|
|
59
|
+
htmlcov/
|
|
60
|
+
|
|
61
|
+
# Editors
|
|
62
|
+
.idea/
|
|
63
|
+
.vscode/
|
|
64
|
+
*.swp
|
|
65
|
+
/docs/plans
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
# CLAUDE.md (`pkg/` workspace)
|
|
2
|
+
|
|
3
|
+
> Sub-CLAUDE.md for the Python package inside the `hyprpilot.nvim`
|
|
4
|
+
> mono-repo. The root [`../CLAUDE.md`](../CLAUDE.md) covers shared
|
|
5
|
+
> conventions (commits, branching, mono-repo layout, release-please).
|
|
6
|
+
> This file covers Python-specific stack, tooling, and gotchas.
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
`hyprpilot-nvim-mcp` is a `uvx`-runnable MCP server that bridges Neovim
|
|
11
|
+
editor state into the [`hyprpilot`](https://github.com/hyprpilot/hyprpilot)
|
|
12
|
+
agent's tool surface. It attaches to a running Neovim via the
|
|
13
|
+
`$NVIM_LISTEN_ADDRESS` socket using `pynvim` and acts as a **pure
|
|
14
|
+
dispatcher**: at boot it queries the Lua side
|
|
15
|
+
(`require("hyprpilot.mcp").list()`) for the captain-registered tool
|
|
16
|
+
catalogue and re-exposes each tool to the agent via FastMCP. The Python
|
|
17
|
+
package ships **zero default tools**; the captain registers what they
|
|
18
|
+
need on the Lua side. Two management tools (`reload_dynamic_tools`,
|
|
19
|
+
`healthcheck`) are the only built-ins.
|
|
20
|
+
|
|
21
|
+
## Stack & Structure
|
|
22
|
+
|
|
23
|
+
- **Language:** Python 3.14+ (`requires-python = ">=3.14"` in
|
|
24
|
+
`pyproject.toml`; ruff `target-version = "py314"`).
|
|
25
|
+
- **CLI:** [`click`](https://click.palletsprojects.com/) with a
|
|
26
|
+
class-based command tree. The `Server` class hosts state + behaviour;
|
|
27
|
+
`Server.cli` is the `@click.group` exposed to `[project.scripts]`.
|
|
28
|
+
click owns env-var parsing for every option (`envvar=`, `click.BOOL`
|
|
29
|
+
coercion). Do not write a separate `Config.from_env()` layer.
|
|
30
|
+
- **Logging:** [`rich`](https://rich.readthedocs.io/) `RichHandler`
|
|
31
|
+
bound to a stderr `Console`. The wrapper module
|
|
32
|
+
(`hyprpilot_nvim_mcp/log.py`) exports `configure(level)` + `get(name)`;
|
|
33
|
+
every module imports `from hyprpilot_nvim_mcp import log` and uses
|
|
34
|
+
the standard logging level methods.
|
|
35
|
+
- **Package manager:** [`uv`](https://docs.astral.sh/uv/) (Astral). This
|
|
36
|
+
package is a member of the repo-root **uv workspace**; `uv.lock` and
|
|
37
|
+
`.venv/` live at the repo root, not in `pkg/`. Run `uv sync` from the
|
|
38
|
+
root (or anywhere — uv finds the workspace).
|
|
39
|
+
- **MCP framework:** [`fastmcp`](https://gofastmcp.com/) — but we don't
|
|
40
|
+
use the decorator path for dynamic tools. We construct
|
|
41
|
+
`fastmcp.tools.FunctionTool(name=..., description=..., parameters=schema, fn=dispatcher)`
|
|
42
|
+
directly so the **Lua schema passes through verbatim** as the agent's
|
|
43
|
+
tool view — single source of truth on the Lua side. Decorator-based
|
|
44
|
+
registration is fine for the management tools (`healthcheck`,
|
|
45
|
+
`reload_dynamic_tools`) where we own both ends.
|
|
46
|
+
- **Neovim client:** [`pynvim`](https://github.com/neovim/pynvim) — msgpack-RPC
|
|
47
|
+
over the listen socket. Sync API.
|
|
48
|
+
- **Lint / format:** `ruff` (single binary, replaces black + isort + flake8).
|
|
49
|
+
- **Type-check:** `mypy --strict` (configured via `pyproject.toml`).
|
|
50
|
+
- **Test:** `pytest` + `pytest-asyncio` (headless `nvim --embed --clean`
|
|
51
|
+
fixture in `conftest.py`).
|
|
52
|
+
- **Toolchain pin:** `mise.toml` pins `python = "3.14"`, `uv = "latest"`,
|
|
53
|
+
`task = "3"`.
|
|
54
|
+
- **Build backend:** `hatchling` (pure-Python wheel; flat package layout).
|
|
55
|
+
- **Layout:**
|
|
56
|
+
`hyprpilot_nvim_mcp/{__init__,cli,log,nvim,server,dispatcher}.py`
|
|
57
|
+
+ `hyprpilot_nvim_mcp/tools/{healthcheck,reload}.py`. `cli.py` hosts
|
|
58
|
+
the `Server` class (click group + runtime state); `dispatcher.py`
|
|
59
|
+
builds the FastMCP `FunctionTool` per Lua-registered tool;
|
|
60
|
+
`tools/<name>.py` exposes a single `register(mcp, nvim, ...)` entry
|
|
61
|
+
point that the CLI wires.
|
|
62
|
+
|
|
63
|
+
## Conventions
|
|
64
|
+
|
|
65
|
+
- **Conventional Commits** — `feat:`, `fix:`, `chore:`, `docs:`,
|
|
66
|
+
`refactor:`, `test:`. Scope when meaningful (`feat(tools): ...`).
|
|
67
|
+
- **Branching** — `feature/*` for new work, `fix/*` or `hotfix/*` for
|
|
68
|
+
fixes.
|
|
69
|
+
- **stdout is sacred** — MCP wire bytes go on stdout. Logs go to stderr
|
|
70
|
+
via the `log` module. **Never** `print()` from a tool; never log to
|
|
71
|
+
stdout. One stray `print` corrupts the daemon's parser.
|
|
72
|
+
- **`mypy --strict` from day one** — every public function annotated,
|
|
73
|
+
every helper too. No `# type: ignore` without a comment explaining why.
|
|
74
|
+
- **One file per management tool** — `tools/<name>.py` exports a
|
|
75
|
+
`register(mcp, nvim, ...)` function and nothing else. Dynamic
|
|
76
|
+
Lua-side tools live in `dispatcher.py` (single closure factory) — no
|
|
77
|
+
per-tool Python file for those.
|
|
78
|
+
- **No tool catalogue in Python** — the captain's tools live on the Lua
|
|
79
|
+
side; Python just dispatches. If you find yourself adding a Python
|
|
80
|
+
tool that talks to nvim editor state (LSP, treesitter, buffers, etc.),
|
|
81
|
+
stop and add it to `lua/hyprpilot/mcp.lua` examples instead. Python
|
|
82
|
+
built-ins are reserved for things only the bridge can do
|
|
83
|
+
(`healthcheck`, `reload_dynamic_tools`).
|
|
84
|
+
- **Errors are values** — wrap every `nvim.*` call to translate pynvim
|
|
85
|
+
exceptions into typed `MCPToolError`. The daemon never sees a Python
|
|
86
|
+
traceback; the captain reads clean messages in the chat surface.
|
|
87
|
+
- **Reconnect, don't crash** — nvim quitting/restarting is normal;
|
|
88
|
+
the bridge survives it. Exponential back-off (1s → 30s cap).
|
|
89
|
+
- **`pyproject.toml` is the single source of truth** for deps, scripts,
|
|
90
|
+
ruff config, mypy config. No `setup.py`, no `setup.cfg`, no
|
|
91
|
+
`requirements.txt`.
|
|
92
|
+
- **Inline single-use values** — same rule as the root CLAUDE.md.
|
|
93
|
+
Never name a local, constant, or intermediate dict that has only one
|
|
94
|
+
consumer. Inline directly into the call site.
|
|
95
|
+
- **Config knobs ship with their behaviour** — never declare a CLI
|
|
96
|
+
option, env var, or config dataclass field whose handler doesn't
|
|
97
|
+
exist yet. Add it in the same PR that wires it.
|
|
98
|
+
- **Validation logs and skips, not throws** for captain-facing input
|
|
99
|
+
(CLI args, env vars, MCP tool registration). Tool handler errors
|
|
100
|
+
still translate to `MCPToolError` per the "Errors are values" rule
|
|
101
|
+
above; that's a different surface.
|
|
102
|
+
|
|
103
|
+
## Decision Log
|
|
104
|
+
|
|
105
|
+
- **Framework: FastMCP**
|
|
106
|
+
- Chose: decorator-based tool registration with auto-derived schemas.
|
|
107
|
+
- Why: zero-ceremony registration, schema comes from type hints +
|
|
108
|
+
docstrings (one source of truth). Upstreamed into the official MCP
|
|
109
|
+
Python SDK.
|
|
110
|
+
- Rejected: hand-rolled JSON-RPC server — boilerplate without payoff.
|
|
111
|
+
|
|
112
|
+
- **Neovim client: pynvim**
|
|
113
|
+
- Chose: official client, msgpack-RPC over the listen socket.
|
|
114
|
+
- Why: idiomatic Python API for buffers/windows/LSP, supports
|
|
115
|
+
`exec_lua` for arbitrary Lua callouts.
|
|
116
|
+
- Rejected: writing our own msgpack-RPC client — reinventing what
|
|
117
|
+
`pynvim` already battle-tests.
|
|
118
|
+
|
|
119
|
+
- **Build backend: hatchling**
|
|
120
|
+
- Chose: hatchling with src layout.
|
|
121
|
+
- Why: `uv` defaults to it for new packages, `pyproject.toml`-only
|
|
122
|
+
config, no quirks.
|
|
123
|
+
- Rejected: setuptools — unnecessary legacy surface for a fresh
|
|
124
|
+
project.
|
|
125
|
+
|
|
126
|
+
- **Sync vs async tools**
|
|
127
|
+
- Chose: synchronous tool handlers.
|
|
128
|
+
- Why: `pynvim`'s msgpack-RPC is sync; FastMCP allows async tools
|
|
129
|
+
but the underlying transport gains nothing from it.
|
|
130
|
+
|
|
131
|
+
- **Pure dispatcher, zero default tools (shipped in #14)**
|
|
132
|
+
- Chose: Python ships only `healthcheck` + `reload_dynamic_tools`.
|
|
133
|
+
Everything else comes from `require("hyprpilot.mcp").list()` on the
|
|
134
|
+
Lua side and is registered via FastMCP `FunctionTool(parameters=schema)`
|
|
135
|
+
so the Lua schema is the agent's view verbatim.
|
|
136
|
+
- Why: single source of truth for the tool catalogue (Lua), and the
|
|
137
|
+
captain registers what they need — no curated Python defaults to
|
|
138
|
+
fight with. Bridge stays small (~150 LOC of dispatcher + nvim wrapper).
|
|
139
|
+
- Rejected: shipping built-in `buffer`/`lsp`/`treesitter`/`exec_lua`
|
|
140
|
+
tools on the Python side. Captain explicitly pulled this:
|
|
141
|
+
"we will register it from explicitly the plugin side of the neovim".
|
|
142
|
+
|
|
143
|
+
- **CLI: class-based click `Server`**
|
|
144
|
+
- Chose: a single `Server` class hosts log config, parsed options,
|
|
145
|
+
and a `serve()` method. `Server.cli` is the `@click.group` exposed
|
|
146
|
+
via `[project.scripts]`. Subcommands receive the instance via
|
|
147
|
+
`click.pass_obj`.
|
|
148
|
+
- Why: keeps option parsing, env-var resolution, and runtime wiring
|
|
149
|
+
in one cohesive unit; matches the captain's pattern across other
|
|
150
|
+
Python projects.
|
|
151
|
+
- Rejected: function-based click commands with separate `Config`
|
|
152
|
+
dataclass — adds an env-parsing layer click already provides.
|
|
153
|
+
|
|
154
|
+
## Approaches Tried
|
|
155
|
+
|
|
156
|
+
- **Built-in `exec_lua` tool (#14)** — gated behind an env var, with a
|
|
157
|
+
separate `tools/exec_lua.py` file. Captain pulled it entirely:
|
|
158
|
+
"we will register it from explicitly the plugin side of the neovim".
|
|
159
|
+
If the captain wants an arbitrary-Lua escape hatch, they register it
|
|
160
|
+
via `require("hyprpilot.mcp").register({...})`. The Python side does
|
|
161
|
+
not own that surface.
|
|
162
|
+
- **Forward-looking config knobs** — same lesson as the root CLAUDE.md.
|
|
163
|
+
Don't declare a CLI option / env var whose handler doesn't exist yet.
|
|
164
|
+
- **Decorator registration for dynamic tools** — `@mcp.tool` derives
|
|
165
|
+
the schema from Python type hints + docstring. For Lua-side tools
|
|
166
|
+
whose schema lives on the Lua side, that's the wrong direction:
|
|
167
|
+
use `FunctionTool(parameters=schema)` so the Lua schema is verbatim.
|
|
168
|
+
|
|
169
|
+
## Tools & MCP Usage
|
|
170
|
+
|
|
171
|
+
- **`uv`** — package + venv manager. `uv sync` installs deps from
|
|
172
|
+
`pyproject.toml` + `uv.lock`. `uvx <pkg>` is the no-install spawn
|
|
173
|
+
idiom (PyPI fetch + cache).
|
|
174
|
+
- **`ruff`** — single-binary lint + format. `task lint` runs both
|
|
175
|
+
`ruff format --check` and `ruff check`.
|
|
176
|
+
- **`mypy --strict`** — full type-check of `hyprpilot_nvim_mcp/` and
|
|
177
|
+
`tests/`. `task lint` runs it after ruff.
|
|
178
|
+
- **`pytest`** — test runner. `tests/conftest.py` spawns headless
|
|
179
|
+
`nvim --embed --clean` per fixture; tests assert against the in-process
|
|
180
|
+
pynvim handle.
|
|
181
|
+
- **`task`** — entry points. `task install`, `task lint`, `task test`,
|
|
182
|
+
`task build`.
|
|
183
|
+
- **`mise`** — pins `python`, `uv`, and `task` versions. CI installs
|
|
184
|
+
via `jdx/mise-action@v4`.
|
|
185
|
+
- **GitHub Actions** — `.github/workflows/ci.yml` (ruff + mypy + pytest)
|
|
186
|
+
and `.github/workflows/release-please.yml` (release-please opens PRs
|
|
187
|
+
on `main` from conventional commits, tags + creates GitHub Release on
|
|
188
|
+
merge).
|
|
189
|
+
|
|
190
|
+
## Publishing to PyPI
|
|
191
|
+
|
|
192
|
+
The `build-pypi` + `publish-pypi` jobs in
|
|
193
|
+
`.github/workflows/release-please.yml` publish `hyprpilot-nvim-mcp`
|
|
194
|
+
to PyPI as a downstream chain of the `release-please` job, gated
|
|
195
|
+
on `release_created == 'true'`. Auth is **PyPI Trusted Publishing
|
|
196
|
+
(OIDC)** — no API token is stored in GitHub. PyPI verifies the
|
|
197
|
+
workflow's identity via OIDC and issues a short-lived publish token
|
|
198
|
+
at runtime.
|
|
199
|
+
|
|
200
|
+
The publish jobs live in `release-please.yml` (not a separate
|
|
201
|
+
`publish-pypi.yml` reusable workflow) because PyPI's trusted-
|
|
202
|
+
publisher matches the OIDC token's `job_workflow_ref` claim
|
|
203
|
+
against the registered workflow file. With `workflow_call`, that
|
|
204
|
+
claim points at the CALLING workflow regardless of where the
|
|
205
|
+
job's steps are defined — so a reusable workflow path requires
|
|
206
|
+
PyPI to register the CALLER's filename, which is confusing and
|
|
207
|
+
error-prone. Inlining keeps OIDC identity = trusted-publisher
|
|
208
|
+
filename = `release-please.yml`.
|
|
209
|
+
|
|
210
|
+
### One-time setup (captain only)
|
|
211
|
+
|
|
212
|
+
> **Migrating from a `publish-pypi.yml`-named publisher?** If you
|
|
213
|
+
> registered the trusted publisher with `Workflow name:
|
|
214
|
+
> publish-pypi.yml` (the pre-PR-#67 setup), update it to
|
|
215
|
+
> `release-please.yml`. PyPI: project settings → "Trusted
|
|
216
|
+
> publishers" → the existing entry → edit the workflow name. No
|
|
217
|
+
> other field changes.
|
|
218
|
+
|
|
219
|
+
**1. Create the PyPI project** (skip if already published).
|
|
220
|
+
The first publish has to seed the project; PyPI doesn't accept a
|
|
221
|
+
trusted publisher for a project that doesn't exist yet. Two paths:
|
|
222
|
+
|
|
223
|
+
- **Recommended — pending publisher.** PyPI lets you register a
|
|
224
|
+
trusted publisher BEFORE the project exists, via
|
|
225
|
+
[pypi.org/manage/account/publishing](https://pypi.org/manage/account/publishing/)
|
|
226
|
+
→ "Add a new pending publisher". Use the values in step 2 below.
|
|
227
|
+
The first tag-push then publishes via OIDC and the pending
|
|
228
|
+
publisher is auto-promoted.
|
|
229
|
+
- **Alternative — manual seed.** Build locally
|
|
230
|
+
(`cd pkg && uv build`) and `uv run twine upload dist/*` once with
|
|
231
|
+
an API token, then add the trusted publisher per step 2.
|
|
232
|
+
|
|
233
|
+
**2. Add the trusted publisher.**
|
|
234
|
+
On [pypi.org/manage/project/hyprpilot-nvim-mcp/settings/publishing/](https://pypi.org/manage/project/hyprpilot-nvim-mcp/settings/publishing/):
|
|
235
|
+
|
|
236
|
+
| Field | Value |
|
|
237
|
+
|---|---|
|
|
238
|
+
| PyPI Project Name | `hyprpilot-nvim-mcp` |
|
|
239
|
+
| Owner | `hyprpilot` |
|
|
240
|
+
| Repository name | `hyprpilot.nvim` |
|
|
241
|
+
| Workflow name | `release-please.yml` |
|
|
242
|
+
| Environment name | `pypi` |
|
|
243
|
+
|
|
244
|
+
The environment binding scopes the OIDC grant to a single
|
|
245
|
+
GitHub Environment, so a workflow running outside `pypi` (a
|
|
246
|
+
forked PR, a misconfigured workflow) can't grab the publish
|
|
247
|
+
token even if it can read the repo.
|
|
248
|
+
|
|
249
|
+
**3. Create the GitHub Environment.**
|
|
250
|
+
On [github.com/hyprpilot/hyprpilot.nvim/settings/environments](https://github.com/hyprpilot/hyprpilot.nvim/settings/environments)
|
|
251
|
+
→ "New environment" → name it `pypi`.
|
|
252
|
+
|
|
253
|
+
Optional but recommended:
|
|
254
|
+
- **Deployment branch rule** → "Selected branches" → add `main`.
|
|
255
|
+
Protects against publishes from feature branches.
|
|
256
|
+
- **Required reviewers** → list the captain. The publish job
|
|
257
|
+
will pause until approved on each tag.
|
|
258
|
+
|
|
259
|
+
**4. Pin the publish action's SHA.**
|
|
260
|
+
The workflow ships with `pypa/gh-action-pypi-publish@release/v1`
|
|
261
|
+
(a moving tag) for the first checkin. Run:
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
gh api repos/pypa/gh-action-pypi-publish/git/refs/tags/release/v1 \
|
|
265
|
+
--jq '.object.sha'
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
…and replace the `release/v1` ref with the resolved SHA, with a
|
|
269
|
+
`# release/v1` trailing comment so Renovate / Dependabot can
|
|
270
|
+
track it (matches the `release-please-action@<sha> # v5.0.0`
|
|
271
|
+
pattern in the same file). Skip this step in dev; ship it
|
|
272
|
+
before the first tagged release.
|
|
273
|
+
|
|
274
|
+
### How it runs
|
|
275
|
+
|
|
276
|
+
1. Captain merges the release-please PR on `main`. release-please
|
|
277
|
+
cuts a `vX.Y.Z` tag and a GitHub Release.
|
|
278
|
+
2. The same workflow's `build-pypi` job (gated on
|
|
279
|
+
`release_created == 'true'`) produces the wheel + sdist via
|
|
280
|
+
`task build` (uv backend); `publish-pypi` downloads the
|
|
281
|
+
artifact and calls `pypa/gh-action-pypi-publish` which
|
|
282
|
+
OIDC-authenticates against PyPI's trusted-publisher entry.
|
|
283
|
+
Both run in the same workflow run as `release-please` — no
|
|
284
|
+
tag-push trigger involved (and none would fire anyway, because
|
|
285
|
+
`secrets.GITHUB_TOKEN`-pushed tags don't trigger workflows by
|
|
286
|
+
GitHub's anti-loop design).
|
|
287
|
+
3. PyPI verifies the OIDC claims (repo, workflow, environment) match
|
|
288
|
+
the registered publisher and accepts the upload. Captain sees
|
|
289
|
+
the new version on
|
|
290
|
+
[pypi.org/project/hyprpilot-nvim-mcp/](https://pypi.org/project/hyprpilot-nvim-mcp/)
|
|
291
|
+
within a minute.
|
|
292
|
+
4. `skip-existing: true` makes a tag re-push (e.g. release-please
|
|
293
|
+
re-cutting the same version) idempotent — the publish step
|
|
294
|
+
no-ops instead of failing.
|
|
295
|
+
|
|
296
|
+
### Why trusted publishing over an API token
|
|
297
|
+
|
|
298
|
+
- **No long-lived secret in the repo.** API tokens get leaked,
|
|
299
|
+
rotated, forgotten. OIDC tokens are minted per-job, valid for
|
|
300
|
+
~15 minutes.
|
|
301
|
+
- **PyPI scopes the grant.** The trusted-publisher entry binds to
|
|
302
|
+
an exact (repo, workflow, environment) tuple. A different
|
|
303
|
+
workflow in the same repo cannot publish, even with admin
|
|
304
|
+
access.
|
|
305
|
+
- **No setup on the captain's local machine.** `pip config` /
|
|
306
|
+
`.pypirc` aren't needed for CI publishing.
|
|
307
|
+
|
|
308
|
+
### Local publishing (if you ever need to)
|
|
309
|
+
|
|
310
|
+
For one-off pushes off the CI path (manual seed in step 1 above,
|
|
311
|
+
hotfix when GitHub Actions is down):
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
cd pkg
|
|
315
|
+
uv build # → dist/*.whl + *.tar.gz
|
|
316
|
+
uv run twine upload dist/* # prompts for an API token
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Generate the token at
|
|
320
|
+
[pypi.org/manage/account/token](https://pypi.org/manage/account/token/);
|
|
321
|
+
scope it to the `hyprpilot-nvim-mcp` project. Stash it in your
|
|
322
|
+
password manager — never commit it, never paste it into a CI
|
|
323
|
+
secret (the workflow uses OIDC; an API token there would be
|
|
324
|
+
strictly worse).
|
|
325
|
+
|
|
326
|
+
## Gotchas
|
|
327
|
+
|
|
328
|
+
- **`NVIM_LISTEN_ADDRESS` must be a Unix socket path** that the running
|
|
329
|
+
Neovim was started with (`nvim --listen /tmp/nvim.sock`). The bridge
|
|
330
|
+
fails fast with a typed error if the socket is missing.
|
|
331
|
+
- **Logging to stdout corrupts the MCP wire** — see Conventions above.
|
|
332
|
+
All logs go to stderr; the `log` module's helpers enforce this.
|
|
333
|
+
- **`pynvim` is thread-unsafe** — the bridge wraps every `nvim.*` access
|
|
334
|
+
in a lock; FastMCP's tool dispatch can run on multiple threads under
|
|
335
|
+
load.
|
|
336
|
+
- **`pynvim` lacks `py.typed`** — mypy can't see its types. We carry
|
|
337
|
+
one targeted `[[tool.mypy.overrides]]` block in `pyproject.toml` to
|
|
338
|
+
silence missing-imports for `pynvim`. Don't widen this to other deps
|
|
339
|
+
without a stated reason.
|
|
340
|
+
- **`FunctionTool` parameters take JSON Schema verbatim** — pass the
|
|
341
|
+
Lua-side `schema` table straight through; do not Pythonize it. FastMCP
|
|
342
|
+
forwards the JSON Schema to the agent. Rebuilding it from Python
|
|
343
|
+
type hints would defeat the purpose of dynamic discovery.
|
|
344
|
+
- **`uv.lock` lives at the repo root**, not in `pkg/`. The workspace
|
|
345
|
+
shares one lockfile and one `.venv/`. Update via `uv sync` or
|
|
346
|
+
`uv lock` from any directory; never edit by hand.
|
|
347
|
+
- **One bridge per nvim** — N Neovim instances mean N entries in
|
|
348
|
+
`mcps.json`, each with its own `NVIM_LISTEN_ADDRESS`. No multi-attach
|
|
349
|
+
in v0.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cenk Kilic
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hyprpilot-nvim-mcp
|
|
3
|
+
Version: 1.1.2
|
|
4
|
+
Summary: MCP server bridging Neovim editor state into the hyprpilot agent's tool surface.
|
|
5
|
+
Project-URL: Homepage, https://github.com/hyprpilot/hyprpilot-nvim-mcp
|
|
6
|
+
Project-URL: Source, https://github.com/hyprpilot/hyprpilot-nvim-mcp
|
|
7
|
+
Author: Cenk Kilic
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Python: >=3.14
|
|
11
|
+
Requires-Dist: click>=8.1
|
|
12
|
+
Requires-Dist: fastmcp>=2.0
|
|
13
|
+
Requires-Dist: pynvim>=0.5
|
|
14
|
+
Requires-Dist: rich>=13.7
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# hyprpilot-nvim-mcp
|
|
18
|
+
|
|
19
|
+
`uvx`-runnable MCP server that bridges Neovim editor state into the
|
|
20
|
+
[`hyprpilot`](https://github.com/hyprpilot/hyprpilot) agent's tool surface.
|
|
21
|
+
|
|
22
|
+
> [!IMPORTANT]
|
|
23
|
+
> This package is in early bootstrap. Only the CLI entry point and a
|
|
24
|
+
> stub `ping` tool are wired today. Buffer / LSP / treesitter / dynamic
|
|
25
|
+
> tool registration land in follow-up PRs (see
|
|
26
|
+
> [`docs/plans/2026-05-09-nvim-mcp-handoff.md`](docs/plans/2026-05-09-nvim-mcp-handoff.md)).
|
|
27
|
+
|
|
28
|
+
## Install in `mcps.json`
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
"neovim": {
|
|
32
|
+
"command": "uvx",
|
|
33
|
+
"args": ["hyprpilot-nvim-mcp"],
|
|
34
|
+
"env": { "NVIM_LISTEN_ADDRESS": "${NVIM_LISTEN_ADDRESS}" }
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`uvx` fetches the package from PyPI on first run and caches it.
|
|
39
|
+
|
|
40
|
+
## Run from source
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
uv sync
|
|
44
|
+
uv run hyprpilot-nvim-mcp
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Environment
|
|
48
|
+
|
|
49
|
+
| Variable | Default | Description |
|
|
50
|
+
| --- | --- | --- |
|
|
51
|
+
| `NVIM_LISTEN_ADDRESS` | _required_ | Path to the running Neovim's listen socket. |
|
|
52
|
+
| `HYPRPILOT_NVIM_MCP_LOG_LEVEL` | `INFO` | Stderr log level. |
|
|
53
|
+
| `HYPRPILOT_NVIM_MCP_ENABLE_EXEC_LUA` | `0` | Enables the `exec_lua` tool (RCE surface — leave off unless you know why). |
|
|
54
|
+
|
|
55
|
+
## Development
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
task install # uv sync --all-groups
|
|
59
|
+
task lint # ruff format --check + ruff check + mypy
|
|
60
|
+
task test # pytest
|
|
61
|
+
task build # uv build
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# hyprpilot-nvim-mcp
|
|
2
|
+
|
|
3
|
+
`uvx`-runnable MCP server that bridges Neovim editor state into the
|
|
4
|
+
[`hyprpilot`](https://github.com/hyprpilot/hyprpilot) agent's tool surface.
|
|
5
|
+
|
|
6
|
+
> [!IMPORTANT]
|
|
7
|
+
> This package is in early bootstrap. Only the CLI entry point and a
|
|
8
|
+
> stub `ping` tool are wired today. Buffer / LSP / treesitter / dynamic
|
|
9
|
+
> tool registration land in follow-up PRs (see
|
|
10
|
+
> [`docs/plans/2026-05-09-nvim-mcp-handoff.md`](docs/plans/2026-05-09-nvim-mcp-handoff.md)).
|
|
11
|
+
|
|
12
|
+
## Install in `mcps.json`
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
"neovim": {
|
|
16
|
+
"command": "uvx",
|
|
17
|
+
"args": ["hyprpilot-nvim-mcp"],
|
|
18
|
+
"env": { "NVIM_LISTEN_ADDRESS": "${NVIM_LISTEN_ADDRESS}" }
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`uvx` fetches the package from PyPI on first run and caches it.
|
|
23
|
+
|
|
24
|
+
## Run from source
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
uv sync
|
|
28
|
+
uv run hyprpilot-nvim-mcp
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Environment
|
|
32
|
+
|
|
33
|
+
| Variable | Default | Description |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| `NVIM_LISTEN_ADDRESS` | _required_ | Path to the running Neovim's listen socket. |
|
|
36
|
+
| `HYPRPILOT_NVIM_MCP_LOG_LEVEL` | `INFO` | Stderr log level. |
|
|
37
|
+
| `HYPRPILOT_NVIM_MCP_ENABLE_EXEC_LUA` | `0` | Enables the `exec_lua` tool (RCE surface — leave off unless you know why). |
|
|
38
|
+
|
|
39
|
+
## Development
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
task install # uv sync --all-groups
|
|
43
|
+
task lint # ruff format --check + ruff check + mypy
|
|
44
|
+
task test # pytest
|
|
45
|
+
task build # uv build
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
# https://taskfile.dev
|
|
3
|
+
|
|
4
|
+
version: "3"
|
|
5
|
+
|
|
6
|
+
tasks:
|
|
7
|
+
install:
|
|
8
|
+
desc: Sync the uv environment.
|
|
9
|
+
cmds:
|
|
10
|
+
- uv sync --all-groups
|
|
11
|
+
|
|
12
|
+
format:
|
|
13
|
+
desc: Format the source tree with ruff.
|
|
14
|
+
cmds:
|
|
15
|
+
- uv run ruff format .
|
|
16
|
+
- uv run ruff check --fix .
|
|
17
|
+
|
|
18
|
+
lint:
|
|
19
|
+
desc: Lint and type-check the source tree.
|
|
20
|
+
cmds:
|
|
21
|
+
- uv run ruff format --check .
|
|
22
|
+
- uv run ruff check .
|
|
23
|
+
- uv run mypy
|
|
24
|
+
|
|
25
|
+
test:
|
|
26
|
+
desc: Run the test suite.
|
|
27
|
+
cmds:
|
|
28
|
+
- uv run pytest
|
|
29
|
+
|
|
30
|
+
build:
|
|
31
|
+
desc: Build wheel and sdist into `pkg/dist/`.
|
|
32
|
+
cmds:
|
|
33
|
+
# `--out-dir dist` (relative to cwd, i.e. `pkg/dist`) overrides
|
|
34
|
+
# uv's default of writing workspace-member artifacts into the
|
|
35
|
+
# workspace ROOT's `dist/`. Local `task build` then drops
|
|
36
|
+
# output inside `pkg/`, and the `publish-pypi.yml` workflow's
|
|
37
|
+
# `upload-artifact path: pkg/dist/` finds them where it
|
|
38
|
+
# expects.
|
|
39
|
+
- uv build --out-dir dist
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""hyprpilot-nvim-mcp — MCP bridge from Neovim editor state into the hyprpilot agent."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = version("hyprpilot-nvim-mcp")
|
|
7
|
+
except PackageNotFoundError:
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
__all__ = ["__version__"]
|