hypruse 0.1.0__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.
- hypruse-0.1.0/.github/workflows/ci.yml +21 -0
- hypruse-0.1.0/.github/workflows/release.yml +54 -0
- hypruse-0.1.0/.gitignore +9 -0
- hypruse-0.1.0/ARCHITECTURE.md +68 -0
- hypruse-0.1.0/CHANGELOG.md +38 -0
- hypruse-0.1.0/CONTRIBUTING.md +35 -0
- hypruse-0.1.0/LICENSE +21 -0
- hypruse-0.1.0/PKG-INFO +189 -0
- hypruse-0.1.0/README.md +165 -0
- hypruse-0.1.0/RELEASING.md +53 -0
- hypruse-0.1.0/packaging/aur/hypruse/PKGBUILD +23 -0
- hypruse-0.1.0/packaging/aur/hypruse-git/PKGBUILD +36 -0
- hypruse-0.1.0/pyproject.toml +52 -0
- hypruse-0.1.0/scripts/e2e_input.py +104 -0
- hypruse-0.1.0/src/hypruse/__init__.py +3 -0
- hypruse-0.1.0/src/hypruse/__main__.py +3 -0
- hypruse-0.1.0/src/hypruse/hyprctl.py +125 -0
- hypruse-0.1.0/src/hypruse/input.py +199 -0
- hypruse-0.1.0/src/hypruse/safety.py +83 -0
- hypruse-0.1.0/src/hypruse/screenshot.py +203 -0
- hypruse-0.1.0/src/hypruse/server.py +275 -0
- hypruse-0.1.0/src/hypruse/session.py +58 -0
- hypruse-0.1.0/src/hypruse/wire.py +237 -0
- hypruse-0.1.0/tests/fixtures/desktop.json +89 -0
- hypruse-0.1.0/tests/test_e2e.py +50 -0
- hypruse-0.1.0/tests/test_fit.py +141 -0
- hypruse-0.1.0/tests/test_hyprctl.py +69 -0
- hypruse-0.1.0/tests/test_input.py +44 -0
- hypruse-0.1.0/tests/test_launch.py +83 -0
- hypruse-0.1.0/tests/test_mcp_roundtrip.py +79 -0
- hypruse-0.1.0/tests/test_prune.py +21 -0
- hypruse-0.1.0/tests/test_safety.py +28 -0
- hypruse-0.1.0/tests/test_screenshot.py +37 -0
- hypruse-0.1.0/tests/test_session.py +63 -0
- hypruse-0.1.0/tests/test_wire.py +61 -0
- hypruse-0.1.0/uv.lock +822 -0
- hypruse-0.1.0/waybar/README.md +46 -0
- hypruse-0.1.0/waybar/hypruse-status.sh +15 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python: ["3.11", "3.14"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: astral-sh/setup-uv@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ matrix.python }}
|
|
19
|
+
- run: uv sync --group dev
|
|
20
|
+
- run: uv run ruff check .
|
|
21
|
+
- run: uv run pytest -q
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI on a version tag via Trusted Publishing (OIDC) — no
|
|
4
|
+
# API token stored anywhere. One-time PyPI setup is documented in
|
|
5
|
+
# RELEASING.md (add a pending publisher for this repo + workflow).
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
push:
|
|
9
|
+
tags: ["v*"]
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: astral-sh/setup-uv@v5
|
|
17
|
+
- run: uv build
|
|
18
|
+
- uses: actions/upload-artifact@v4
|
|
19
|
+
with:
|
|
20
|
+
name: dist
|
|
21
|
+
path: dist/
|
|
22
|
+
|
|
23
|
+
publish:
|
|
24
|
+
needs: build
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
environment: pypi
|
|
27
|
+
permissions:
|
|
28
|
+
id-token: write # OIDC token for Trusted Publishing
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/download-artifact@v4
|
|
31
|
+
with:
|
|
32
|
+
name: dist
|
|
33
|
+
path: dist/
|
|
34
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
35
|
+
|
|
36
|
+
github-release:
|
|
37
|
+
needs: publish
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
permissions:
|
|
40
|
+
contents: write
|
|
41
|
+
steps:
|
|
42
|
+
- uses: actions/checkout@v4
|
|
43
|
+
- uses: actions/download-artifact@v4
|
|
44
|
+
with:
|
|
45
|
+
name: dist
|
|
46
|
+
path: dist/
|
|
47
|
+
- name: Create GitHub release
|
|
48
|
+
env:
|
|
49
|
+
GH_TOKEN: ${{ github.token }}
|
|
50
|
+
run: >
|
|
51
|
+
gh release create "${GITHUB_REF_NAME}"
|
|
52
|
+
--title "${GITHUB_REF_NAME}"
|
|
53
|
+
--notes "See [CHANGELOG.md](CHANGELOG.md)."
|
|
54
|
+
dist/*
|
hypruse-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
Ten-minute orientation for contributors.
|
|
4
|
+
|
|
5
|
+
## Module map
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
src/hypruse/
|
|
9
|
+
server.py MCP wiring: 6 tools, docstrings = the agent-facing API
|
|
10
|
+
hyprctl.py all Hyprland IPC (queries + dispatchers), state trimming
|
|
11
|
+
wire.py raw Wayland client for zwlr_virtual_pointer_v1
|
|
12
|
+
input.py pointer orchestration (movecursor + wire) and wtype keyboard
|
|
13
|
+
screenshot.py grim capture: monitor / window / region + coord metadata
|
|
14
|
+
session.py discovers HYPRLAND_INSTANCE_SIGNATURE / WAYLAND_DISPLAY
|
|
15
|
+
from runtime-dir sockets when the host stripped the env
|
|
16
|
+
safety.py activity beacon + kill-switch semantics
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Rule of thumb: `server.py` validates and narrates; everything real happens
|
|
20
|
+
in the leaf modules, which stay importable and testable without MCP.
|
|
21
|
+
|
|
22
|
+
## The coordinate contract
|
|
23
|
+
|
|
24
|
+
One space rules everything: **Hyprland global logical coordinates** (what
|
|
25
|
+
`hyprctl cursorpos`, client `at`, and `dispatch movecursor` speak).
|
|
26
|
+
|
|
27
|
+
- `desktop` reports window geometry in it.
|
|
28
|
+
- `pointer` accepts it.
|
|
29
|
+
- `screenshot` captures *pixels* and returns `geometry` + `scale` per
|
|
30
|
+
capture so callers map back: `global = origin + pixel / scale`.
|
|
31
|
+
|
|
32
|
+
If you touch anything coordinate-adjacent, preserve this contract; it is
|
|
33
|
+
what keeps multi-monitor and fractional scaling tractable.
|
|
34
|
+
|
|
35
|
+
## Why input works the way it does
|
|
36
|
+
|
|
37
|
+
- **Position** via `hyprctl dispatch movecursor x y` — authoritative,
|
|
38
|
+
global, no per-monitor extent math, immune to
|
|
39
|
+
[hyprwm/Hyprland#6749](https://github.com/hyprwm/Hyprland/issues/6749).
|
|
40
|
+
- **Buttons/axis** via a virtual pointer created over the raw wire
|
|
41
|
+
(`wire.py` is ~250 lines: registry scan, bind, button/axis/frame, sync
|
|
42
|
+
barrier, wl_display.error surfacing). No daemon, no uinput, no root.
|
|
43
|
+
- **Keyboard** via `wtype`: it uploads its own XKB keymap through
|
|
44
|
+
`zwp_virtual_keyboard_v1`, which is why unicode and non-US layouts work.
|
|
45
|
+
We shell out instead of reimplementing keymap upload — that wheel is
|
|
46
|
+
round already.
|
|
47
|
+
|
|
48
|
+
A press and its release always happen inside one tool call, which is what
|
|
49
|
+
makes `pkill -f hypruse` a safe panic action at any moment.
|
|
50
|
+
|
|
51
|
+
## Sequence of a typical agent step
|
|
52
|
+
|
|
53
|
+
1. `desktop` → find `firefox` at `0x…`, workspace 3, geometry.
|
|
54
|
+
2. `hypr focus_window 0x…` (IPC, ~ms) — no vision spent.
|
|
55
|
+
3. `screenshot window=0x…` → crop + `geometry`/`scale`.
|
|
56
|
+
4. `pointer click x y` — computed from image pixel via the contract.
|
|
57
|
+
5. `keyboard type "…"`.
|
|
58
|
+
6. `desktop` again to verify the world changed as expected.
|
|
59
|
+
|
|
60
|
+
## Testing tiers
|
|
61
|
+
|
|
62
|
+
1. **Unit** (CI): pure functions — wire encoders/parsers, combo parsing,
|
|
63
|
+
region parsing, state trimming against fixtures.
|
|
64
|
+
2. **Live seat-safe** (`pytest -m e2e`): real session, zero input events —
|
|
65
|
+
including a virtual-pointer create/destroy handshake.
|
|
66
|
+
3. **Supervised** (`scripts/e2e_input.py`): the only tier that clicks and
|
|
67
|
+
types; countdown, self-verifying via kitty remote control, restores
|
|
68
|
+
focus. Never wire this into anything automatic.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
|
|
5
|
+
[SemVer](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] — 2026-07-16
|
|
10
|
+
|
|
11
|
+
Initial release.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- MCP stdio server with six tools: `desktop`, `screenshot`, `pointer`,
|
|
15
|
+
`keyboard`, `hypr`, `launch`, plus cross-tool guidance in the server
|
|
16
|
+
`instructions` and terse per-tool schemas for eager client loading.
|
|
17
|
+
- Semantic desktop snapshot over hyprctl IPC (token-lean, fixture-tested).
|
|
18
|
+
- Screenshots via grim: focused monitor, exact window crop, region zoom,
|
|
19
|
+
with true output dimensions and scale for exact pixel→global mapping.
|
|
20
|
+
Default returns a saved PNG path (works with hosts that mangle inline
|
|
21
|
+
image blocks); `HYPRUSE_SCREENSHOT_MODE=image` returns wire-level image
|
|
22
|
+
content auto-fit to a transport byte budget and long-edge cap
|
|
23
|
+
(`HYPRUSE_MAX_IMAGE_BYTES`, `HYPRUSE_MAX_IMAGE_EDGE`) by degrading format
|
|
24
|
+
before resolution, so the API never silently downscales under the model.
|
|
25
|
+
Coarse-to-fine clicking guidance to counter pixel-estimation error.
|
|
26
|
+
- Pointer input over a raw `zwlr_virtual_pointer_v1` wire client — no
|
|
27
|
+
ydotool, no uinput, no daemon; positioning via `hyprctl movecursor`.
|
|
28
|
+
- Keyboard input via wtype (XKB keymap upload; unicode/layout-correct),
|
|
29
|
+
with combo parsing (`ctrl+shift+t`, `super+enter`, bare-mod taps).
|
|
30
|
+
- Workspace/window dispatchers and app launch with new-window detection,
|
|
31
|
+
including single-instance apps (browsers) whose window is relocated to
|
|
32
|
+
the requested workspace.
|
|
33
|
+
- Session discovery: finds `HYPRLAND_INSTANCE_SIGNATURE` / `WAYLAND_DISPLAY`
|
|
34
|
+
from runtime sockets when the host launches the server with a stripped
|
|
35
|
+
environment (dbus/systemd-activated desktop apps).
|
|
36
|
+
- Activity beacon + Waybar kill-switch indicator module.
|
|
37
|
+
- Unit suite, seat-safe live e2e, MCP stdio round-trip tests, supervised
|
|
38
|
+
input verification script, CI.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Contributions are welcome — especially the roadmap items marked
|
|
4
|
+
`help wanted` (sway/niri support is the big one: `wire.py` already speaks
|
|
5
|
+
the wlr protocols; what's missing is an IPC layer equivalent to
|
|
6
|
+
`hyprctl.py` for those compositors).
|
|
7
|
+
|
|
8
|
+
## Setup
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
git clone https://github.com/IlyasKhallouki/hypruse
|
|
12
|
+
cd hypruse
|
|
13
|
+
uv sync --group dev
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Before you push
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
uv run ruff check .
|
|
20
|
+
uv run pytest # must stay green without a compositor
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
If your change touches live behaviour, also run `uv run pytest -m e2e
|
|
24
|
+
--override-ini addopts=` inside a Hyprland session, and
|
|
25
|
+
`scripts/e2e_input.py` if it touches input (supervised — it takes the
|
|
26
|
+
seat for ~10 seconds).
|
|
27
|
+
|
|
28
|
+
## Ground rules
|
|
29
|
+
|
|
30
|
+
- Conventional commits (`feat:`, `fix:`, `docs:`, `test:`, `ci:`, `chore:`).
|
|
31
|
+
- Unit tests accompany features; anything pure gets tested pure.
|
|
32
|
+
- Never make input tests automatic. The supervised script stays supervised.
|
|
33
|
+
- Keep the coordinate contract (see ARCHITECTURE.md) intact.
|
|
34
|
+
- No new runtime dependencies without an issue discussion first — the
|
|
35
|
+
zero-daemon, near-zero-dep footprint is a feature.
|
hypruse-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ilyas Khallouki
|
|
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.
|
hypruse-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hypruse
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Computer use for Hyprland — an MCP server that gives AI agents native hands on your Wayland desktop
|
|
5
|
+
Project-URL: Homepage, https://github.com/IlyasKhallouki/hypruse
|
|
6
|
+
Project-URL: Issues, https://github.com/IlyasKhallouki/hypruse/issues
|
|
7
|
+
Author-email: Ilyas Khallouki <khalloukielias@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: ai-agents,automation,claude,computer-use,hyprland,mcp,wayland
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: X11 Applications
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Desktop Environment :: Window Managers
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: mcp>=1.2
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# hypruse
|
|
26
|
+
|
|
27
|
+
**Computer use for [Hyprland](https://hypr.land).** An [MCP](https://modelcontextprotocol.io) server that gives AI agents native hands on your Wayland desktop — workspaces, windows, mouse, keyboard, screenshots.
|
|
28
|
+
|
|
29
|
+
No ydotool daemon. No root. No portals. No X11.
|
|
30
|
+
|
|
31
|
+
<!-- demo: assets/demo.gif goes here before launch -->
|
|
32
|
+
|
|
33
|
+
## Why
|
|
34
|
+
|
|
35
|
+
Computer use exists on macOS and Windows. On Linux there is effectively nothing: the Claude Desktop Linux beta explicitly ships **without** screen control, Anthropic's reference implementation is an X11 container, and the existing Wayland attempts lean on setuid uinput hacks or GNOME-only portals.
|
|
36
|
+
|
|
37
|
+
Meanwhile Hyprland already exposes everything an agent needs, better than any accessibility bridge: a complete IPC surface for state and window management, and first-class Wayland protocols for input. hypruse just wires them to MCP:
|
|
38
|
+
|
|
39
|
+
- **Semantic first.** `desktop` returns the real window/workspace tree — addresses, classes, titles, geometry — in one call. The agent switches workspaces and focuses windows the way you do (instantly, over IPC), not by squinting at pixels.
|
|
40
|
+
- **Vision when it matters.** Screenshots of a monitor, an exact window crop, or a zoomed region, with the geometry/scale metadata to map any pixel back to a clickable coordinate.
|
|
41
|
+
- **Native input.** Clicks and scrolls are spoken directly over the Wayland wire (`zwlr_virtual_pointer_v1`); typing goes through `wtype`'s virtual keyboard with a proper XKB keymap — unicode-safe on any layout.
|
|
42
|
+
|
|
43
|
+
## How it works
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
agent (Claude Code, or any MCP client)
|
|
47
|
+
│ stdio
|
|
48
|
+
▼
|
|
49
|
+
hypruse
|
|
50
|
+
├── hyprctl -j ········▶ desktop state: monitors, workspaces, windows
|
|
51
|
+
├── hyprctl dispatch ··▶ focus / move / close / launch / movecursor
|
|
52
|
+
├── grim ··············▶ screenshots: monitor, window crop, region
|
|
53
|
+
├── wtype ·············▶ keyboard (zwp_virtual_keyboard_v1, real XKB keymap)
|
|
54
|
+
└── raw Wayland wire ··▶ click & scroll (zwlr_virtual_pointer_v1)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Design choices, defended:
|
|
58
|
+
|
|
59
|
+
- **No ydotool / uinput.** That path needs a daemon, udev rules or root, and types US scancodes that break on other layouts. hypruse is just another Wayland client of your compositor — same standing as `wlrctl`.
|
|
60
|
+
- **No portals.** `xdg-desktop-portal-hyprland` does not implement the RemoteDesktop portal (InputCapture is capture, not injection), so anything built on libei/portals silently degrades on Hyprland. hypruse doesn't try.
|
|
61
|
+
- **Cursor positioning via `hyprctl dispatch movecursor`** (global logical coordinates, exact on any monitor layout), with only button/axis events on the virtual pointer — sidestepping the known multi-monitor bugs of absolute virtual-pointer motion ([hyprwm/Hyprland#6749](https://github.com/hyprwm/Hyprland/issues/6749)).
|
|
62
|
+
|
|
63
|
+
## Tools
|
|
64
|
+
|
|
65
|
+
| tool | what it does |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `desktop` | One-call semantic snapshot: monitors, workspaces, windows (address/class/title/geometry), active window, cursor |
|
|
68
|
+
| `screenshot` | Focused monitor, exact window crop by address, or `x,y,WxH` region — returns image + coordinate-mapping metadata |
|
|
69
|
+
| `pointer` | move / click / drag / scroll in global coordinates |
|
|
70
|
+
| `keyboard` | Type literal text (unicode-safe) or press combos: `ctrl+shift+t`, `super+enter`, `F5` |
|
|
71
|
+
| `hypr` | Switch workspace, focus/move/close windows, fullscreen, floating — pure IPC, milliseconds |
|
|
72
|
+
| `launch` | Start an app (optionally silent on another workspace), wait for its window, return its address — detects single-instance apps (browsers) whose window ignores exec rules, and moves it to the requested workspace |
|
|
73
|
+
|
|
74
|
+
## Install
|
|
75
|
+
|
|
76
|
+
Requirements: Hyprland, `grim`, `wtype` (most Hyprland setups already have both), and [uv](https://docs.astral.sh/uv/).
|
|
77
|
+
|
|
78
|
+
Claude Code:
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
claude mcp add -s user hypruse -- uvx hypruse
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
From a source checkout:
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
claude mcp add -s user hypruse -- uv run --directory /path/to/hypruse hypruse
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Any other MCP client: run `uvx hypruse` as a stdio server.
|
|
91
|
+
|
|
92
|
+
### Claude Desktop (Linux beta)
|
|
93
|
+
|
|
94
|
+
The Linux beta ships **without** Anthropic's first-party computer use — but stdio MCP servers work in chat, which makes hypruse the workaround. In `~/.config/Claude/claude_desktop_config.json`:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"mcpServers": {
|
|
99
|
+
"hypruse": {
|
|
100
|
+
"command": "uvx",
|
|
101
|
+
"args": ["hypruse"],
|
|
102
|
+
"env": { "HYPRUSE_SCREENSHOT_MODE": "image" }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Two Desktop-specific notes: use `image` mode (Desktop renders inline MCP images and has no file-read tool), and the app must run natively inside your Hyprland session so the server inherits `WAYLAND_DISPLAY`/`HYPRLAND_INSTANCE_SIGNATURE` — from a VM or container it cannot reach your compositor. If your Desktop install bypasses tool-approval prompts, treat the [Waybar indicator + panic keybind](waybar/) as mandatory, not optional.
|
|
109
|
+
|
|
110
|
+
## Security model
|
|
111
|
+
|
|
112
|
+
Read this section before installing. **hypruse hands an agent your mouse, your keyboard, your screen contents, and an app launcher.** The layers that keep that sane:
|
|
113
|
+
|
|
114
|
+
1. **Approval** — MCP clients gate tool calls. In Claude Code, allowlist the read-only tools (`desktop`, `screenshot`) and leave `pointer`/`keyboard`/`hypr`/`launch` on ask-first until you trust a workflow.
|
|
115
|
+
2. **Visibility** — the server maintains an activity beacon (`$XDG_RUNTIME_DIR/hypruse/state.json`); the shipped [Waybar module](waybar/) is invisible when idle and shows while an agent has hands on your desktop.
|
|
116
|
+
3. **Interruption** — click the indicator, or bind a panic key: `bind = SUPER SHIFT, BackSpace, exec, pkill -f hypruse`. Killing it mid-action is safe: button press/release pairs never span tool calls, so it cannot die holding a button.
|
|
117
|
+
4. **The seat is shared.** There is one cursor and one keyboard focus, and Hyprland's focus-follows-mouse means a cursor move alone can retarget keystrokes. Don't type while an agent is driving — watch the indicator.
|
|
118
|
+
5. **Scope** — stdio only (no network listener), no clipboard access, nothing persisted except the beacon. A screenshot sees everything visible: treat an agent session like screen sharing.
|
|
119
|
+
|
|
120
|
+
## Performance
|
|
121
|
+
|
|
122
|
+
Measured on a live session (Hyprland 0.55, 1080p, 20 windows): `desktop`
|
|
123
|
+
~30 ms, workspace/window dispatch ~10–20 ms, screenshots ~0.5 s. If tool
|
|
124
|
+
calls *feel* slow, it is almost certainly the MCP **approval prompt** in
|
|
125
|
+
front of each call, not the server — allowlist the tools you trust and the
|
|
126
|
+
latency disappears. Claude Code (`.claude/settings.json`):
|
|
127
|
+
|
|
128
|
+
```jsonc
|
|
129
|
+
{
|
|
130
|
+
"permissions": {
|
|
131
|
+
"allow": [
|
|
132
|
+
"mcp__hypruse__desktop",
|
|
133
|
+
"mcp__hypruse__screenshot",
|
|
134
|
+
"mcp__hypruse__hypr"
|
|
135
|
+
// add pointer/keyboard/launch once you trust your workflows
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Coordinates
|
|
142
|
+
|
|
143
|
+
Everything speaks Hyprland's global logical coordinates — the space `hyprctl cursorpos` and window `at` use. Screenshots are pixel-space; each capture returns `geometry` and `scale` so `global = origin + pixel / scale`. On scale 1.0 monitors (most setups) image pixels *are* global coordinates.
|
|
144
|
+
|
|
145
|
+
In image mode, captures automatically fit the host's result-size limit (Claude Desktop caps tool results at 1 MB): format degrades before resolution — native PNG, then full-res JPEG, then stepped downscale — because full-res JPEG reads UI text better than half-res PNG. The applied scale is folded into the returned metadata, so coordinate mapping stays exact; tune with `HYPRUSE_MAX_IMAGE_BYTES`, or pass `scale` for a deliberate zoom-out.
|
|
146
|
+
|
|
147
|
+
By default the screenshot tool writes a PNG under `$XDG_RUNTIME_DIR/hypruse/` and returns its path — MCP hosts with a file reader (Claude Code's `Read`) render it natively. This default exists because some hosts (Claude Code ≤ 2.1.x among them) serialize inline MCP image blocks to base64 *text* the model can't see; we verified this empirically rather than trusting the spec. `HYPRUSE_SCREENSHOT_MODE=image` switches to inline image content blocks for hosts that render them correctly.
|
|
148
|
+
|
|
149
|
+
## Development
|
|
150
|
+
|
|
151
|
+
```sh
|
|
152
|
+
uv sync --group dev
|
|
153
|
+
uv run pytest # unit tests, no compositor needed
|
|
154
|
+
uv run pytest -m e2e --override-ini addopts= # live seat-safe checks
|
|
155
|
+
uv run python scripts/e2e_input.py # supervised: takes the seat ~10s
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The input e2e is deliberately manual — it borrows your cursor and keyboard, counts down, proves click/scroll/type delivery by reading the target terminal's screen back over kitty remote control, and restores your focus.
|
|
159
|
+
|
|
160
|
+
## Roadmap
|
|
161
|
+
|
|
162
|
+
**Now — distribution**
|
|
163
|
+
- [ ] PyPI (`uvx hypruse`) and AUR (`hypruse`, `hypruse-git`)
|
|
164
|
+
- [ ] Demo GIF, then `awesome-mcp-servers` / `awesome-hyprland` listings
|
|
165
|
+
|
|
166
|
+
**Next — trust & accuracy**
|
|
167
|
+
- [ ] `hypruse doctor` — first-run diagnostics (deps, session reachability, virtual-pointer handshake)
|
|
168
|
+
- [ ] Click-by-text via OCR (Tesseract) — click labels, not estimated pixels; works on any app
|
|
169
|
+
- [ ] Read-only mode — disable input tools for a safe first run
|
|
170
|
+
- [ ] Headless-Hyprland CI — real end-to-end tests in GitHub Actions
|
|
171
|
+
|
|
172
|
+
**Then — breadth & depth**
|
|
173
|
+
- [ ] sway / niri support — the wire client already speaks the wlr protocols; needs an IPC layer alongside `hyprctl.py` (PRs very welcome, `help wanted`)
|
|
174
|
+
- [ ] AT-SPI element tree — click by accessible name, read GTK/Qt UIs without vision
|
|
175
|
+
- [ ] Clipboard (`wl-clipboard`), settle / wait-for-stable, discrete-axis scroll
|
|
176
|
+
- [ ] Multi-monitor and fractional-scaling hardening
|
|
177
|
+
|
|
178
|
+
## Alternatives, honestly
|
|
179
|
+
|
|
180
|
+
| project | approach | where it falls short on Hyprland |
|
|
181
|
+
|---|---|---|
|
|
182
|
+
| computer-use-linux | AT-SPI + portals, ydotool fallback | GNOME-first; RemoteDesktop portal is dead on Hyprland, screenshots portal-first |
|
|
183
|
+
| hyprmcp | hyprctl wrapper | no screenshots, no input |
|
|
184
|
+
| wayland-mcp (×2) | setuid evemu + bundled VLM | archived / stale; evemu needs elevated setup |
|
|
185
|
+
| Anthropic computer-use-demo | X11 + xdotool in Docker | a sandboxed demo, not your desktop |
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
[MIT](LICENSE)
|
hypruse-0.1.0/README.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# hypruse
|
|
2
|
+
|
|
3
|
+
**Computer use for [Hyprland](https://hypr.land).** An [MCP](https://modelcontextprotocol.io) server that gives AI agents native hands on your Wayland desktop — workspaces, windows, mouse, keyboard, screenshots.
|
|
4
|
+
|
|
5
|
+
No ydotool daemon. No root. No portals. No X11.
|
|
6
|
+
|
|
7
|
+
<!-- demo: assets/demo.gif goes here before launch -->
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
Computer use exists on macOS and Windows. On Linux there is effectively nothing: the Claude Desktop Linux beta explicitly ships **without** screen control, Anthropic's reference implementation is an X11 container, and the existing Wayland attempts lean on setuid uinput hacks or GNOME-only portals.
|
|
12
|
+
|
|
13
|
+
Meanwhile Hyprland already exposes everything an agent needs, better than any accessibility bridge: a complete IPC surface for state and window management, and first-class Wayland protocols for input. hypruse just wires them to MCP:
|
|
14
|
+
|
|
15
|
+
- **Semantic first.** `desktop` returns the real window/workspace tree — addresses, classes, titles, geometry — in one call. The agent switches workspaces and focuses windows the way you do (instantly, over IPC), not by squinting at pixels.
|
|
16
|
+
- **Vision when it matters.** Screenshots of a monitor, an exact window crop, or a zoomed region, with the geometry/scale metadata to map any pixel back to a clickable coordinate.
|
|
17
|
+
- **Native input.** Clicks and scrolls are spoken directly over the Wayland wire (`zwlr_virtual_pointer_v1`); typing goes through `wtype`'s virtual keyboard with a proper XKB keymap — unicode-safe on any layout.
|
|
18
|
+
|
|
19
|
+
## How it works
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
agent (Claude Code, or any MCP client)
|
|
23
|
+
│ stdio
|
|
24
|
+
▼
|
|
25
|
+
hypruse
|
|
26
|
+
├── hyprctl -j ········▶ desktop state: monitors, workspaces, windows
|
|
27
|
+
├── hyprctl dispatch ··▶ focus / move / close / launch / movecursor
|
|
28
|
+
├── grim ··············▶ screenshots: monitor, window crop, region
|
|
29
|
+
├── wtype ·············▶ keyboard (zwp_virtual_keyboard_v1, real XKB keymap)
|
|
30
|
+
└── raw Wayland wire ··▶ click & scroll (zwlr_virtual_pointer_v1)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Design choices, defended:
|
|
34
|
+
|
|
35
|
+
- **No ydotool / uinput.** That path needs a daemon, udev rules or root, and types US scancodes that break on other layouts. hypruse is just another Wayland client of your compositor — same standing as `wlrctl`.
|
|
36
|
+
- **No portals.** `xdg-desktop-portal-hyprland` does not implement the RemoteDesktop portal (InputCapture is capture, not injection), so anything built on libei/portals silently degrades on Hyprland. hypruse doesn't try.
|
|
37
|
+
- **Cursor positioning via `hyprctl dispatch movecursor`** (global logical coordinates, exact on any monitor layout), with only button/axis events on the virtual pointer — sidestepping the known multi-monitor bugs of absolute virtual-pointer motion ([hyprwm/Hyprland#6749](https://github.com/hyprwm/Hyprland/issues/6749)).
|
|
38
|
+
|
|
39
|
+
## Tools
|
|
40
|
+
|
|
41
|
+
| tool | what it does |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `desktop` | One-call semantic snapshot: monitors, workspaces, windows (address/class/title/geometry), active window, cursor |
|
|
44
|
+
| `screenshot` | Focused monitor, exact window crop by address, or `x,y,WxH` region — returns image + coordinate-mapping metadata |
|
|
45
|
+
| `pointer` | move / click / drag / scroll in global coordinates |
|
|
46
|
+
| `keyboard` | Type literal text (unicode-safe) or press combos: `ctrl+shift+t`, `super+enter`, `F5` |
|
|
47
|
+
| `hypr` | Switch workspace, focus/move/close windows, fullscreen, floating — pure IPC, milliseconds |
|
|
48
|
+
| `launch` | Start an app (optionally silent on another workspace), wait for its window, return its address — detects single-instance apps (browsers) whose window ignores exec rules, and moves it to the requested workspace |
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
Requirements: Hyprland, `grim`, `wtype` (most Hyprland setups already have both), and [uv](https://docs.astral.sh/uv/).
|
|
53
|
+
|
|
54
|
+
Claude Code:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
claude mcp add -s user hypruse -- uvx hypruse
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
From a source checkout:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
claude mcp add -s user hypruse -- uv run --directory /path/to/hypruse hypruse
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Any other MCP client: run `uvx hypruse` as a stdio server.
|
|
67
|
+
|
|
68
|
+
### Claude Desktop (Linux beta)
|
|
69
|
+
|
|
70
|
+
The Linux beta ships **without** Anthropic's first-party computer use — but stdio MCP servers work in chat, which makes hypruse the workaround. In `~/.config/Claude/claude_desktop_config.json`:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"mcpServers": {
|
|
75
|
+
"hypruse": {
|
|
76
|
+
"command": "uvx",
|
|
77
|
+
"args": ["hypruse"],
|
|
78
|
+
"env": { "HYPRUSE_SCREENSHOT_MODE": "image" }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Two Desktop-specific notes: use `image` mode (Desktop renders inline MCP images and has no file-read tool), and the app must run natively inside your Hyprland session so the server inherits `WAYLAND_DISPLAY`/`HYPRLAND_INSTANCE_SIGNATURE` — from a VM or container it cannot reach your compositor. If your Desktop install bypasses tool-approval prompts, treat the [Waybar indicator + panic keybind](waybar/) as mandatory, not optional.
|
|
85
|
+
|
|
86
|
+
## Security model
|
|
87
|
+
|
|
88
|
+
Read this section before installing. **hypruse hands an agent your mouse, your keyboard, your screen contents, and an app launcher.** The layers that keep that sane:
|
|
89
|
+
|
|
90
|
+
1. **Approval** — MCP clients gate tool calls. In Claude Code, allowlist the read-only tools (`desktop`, `screenshot`) and leave `pointer`/`keyboard`/`hypr`/`launch` on ask-first until you trust a workflow.
|
|
91
|
+
2. **Visibility** — the server maintains an activity beacon (`$XDG_RUNTIME_DIR/hypruse/state.json`); the shipped [Waybar module](waybar/) is invisible when idle and shows while an agent has hands on your desktop.
|
|
92
|
+
3. **Interruption** — click the indicator, or bind a panic key: `bind = SUPER SHIFT, BackSpace, exec, pkill -f hypruse`. Killing it mid-action is safe: button press/release pairs never span tool calls, so it cannot die holding a button.
|
|
93
|
+
4. **The seat is shared.** There is one cursor and one keyboard focus, and Hyprland's focus-follows-mouse means a cursor move alone can retarget keystrokes. Don't type while an agent is driving — watch the indicator.
|
|
94
|
+
5. **Scope** — stdio only (no network listener), no clipboard access, nothing persisted except the beacon. A screenshot sees everything visible: treat an agent session like screen sharing.
|
|
95
|
+
|
|
96
|
+
## Performance
|
|
97
|
+
|
|
98
|
+
Measured on a live session (Hyprland 0.55, 1080p, 20 windows): `desktop`
|
|
99
|
+
~30 ms, workspace/window dispatch ~10–20 ms, screenshots ~0.5 s. If tool
|
|
100
|
+
calls *feel* slow, it is almost certainly the MCP **approval prompt** in
|
|
101
|
+
front of each call, not the server — allowlist the tools you trust and the
|
|
102
|
+
latency disappears. Claude Code (`.claude/settings.json`):
|
|
103
|
+
|
|
104
|
+
```jsonc
|
|
105
|
+
{
|
|
106
|
+
"permissions": {
|
|
107
|
+
"allow": [
|
|
108
|
+
"mcp__hypruse__desktop",
|
|
109
|
+
"mcp__hypruse__screenshot",
|
|
110
|
+
"mcp__hypruse__hypr"
|
|
111
|
+
// add pointer/keyboard/launch once you trust your workflows
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Coordinates
|
|
118
|
+
|
|
119
|
+
Everything speaks Hyprland's global logical coordinates — the space `hyprctl cursorpos` and window `at` use. Screenshots are pixel-space; each capture returns `geometry` and `scale` so `global = origin + pixel / scale`. On scale 1.0 monitors (most setups) image pixels *are* global coordinates.
|
|
120
|
+
|
|
121
|
+
In image mode, captures automatically fit the host's result-size limit (Claude Desktop caps tool results at 1 MB): format degrades before resolution — native PNG, then full-res JPEG, then stepped downscale — because full-res JPEG reads UI text better than half-res PNG. The applied scale is folded into the returned metadata, so coordinate mapping stays exact; tune with `HYPRUSE_MAX_IMAGE_BYTES`, or pass `scale` for a deliberate zoom-out.
|
|
122
|
+
|
|
123
|
+
By default the screenshot tool writes a PNG under `$XDG_RUNTIME_DIR/hypruse/` and returns its path — MCP hosts with a file reader (Claude Code's `Read`) render it natively. This default exists because some hosts (Claude Code ≤ 2.1.x among them) serialize inline MCP image blocks to base64 *text* the model can't see; we verified this empirically rather than trusting the spec. `HYPRUSE_SCREENSHOT_MODE=image` switches to inline image content blocks for hosts that render them correctly.
|
|
124
|
+
|
|
125
|
+
## Development
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
uv sync --group dev
|
|
129
|
+
uv run pytest # unit tests, no compositor needed
|
|
130
|
+
uv run pytest -m e2e --override-ini addopts= # live seat-safe checks
|
|
131
|
+
uv run python scripts/e2e_input.py # supervised: takes the seat ~10s
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The input e2e is deliberately manual — it borrows your cursor and keyboard, counts down, proves click/scroll/type delivery by reading the target terminal's screen back over kitty remote control, and restores your focus.
|
|
135
|
+
|
|
136
|
+
## Roadmap
|
|
137
|
+
|
|
138
|
+
**Now — distribution**
|
|
139
|
+
- [ ] PyPI (`uvx hypruse`) and AUR (`hypruse`, `hypruse-git`)
|
|
140
|
+
- [ ] Demo GIF, then `awesome-mcp-servers` / `awesome-hyprland` listings
|
|
141
|
+
|
|
142
|
+
**Next — trust & accuracy**
|
|
143
|
+
- [ ] `hypruse doctor` — first-run diagnostics (deps, session reachability, virtual-pointer handshake)
|
|
144
|
+
- [ ] Click-by-text via OCR (Tesseract) — click labels, not estimated pixels; works on any app
|
|
145
|
+
- [ ] Read-only mode — disable input tools for a safe first run
|
|
146
|
+
- [ ] Headless-Hyprland CI — real end-to-end tests in GitHub Actions
|
|
147
|
+
|
|
148
|
+
**Then — breadth & depth**
|
|
149
|
+
- [ ] sway / niri support — the wire client already speaks the wlr protocols; needs an IPC layer alongside `hyprctl.py` (PRs very welcome, `help wanted`)
|
|
150
|
+
- [ ] AT-SPI element tree — click by accessible name, read GTK/Qt UIs without vision
|
|
151
|
+
- [ ] Clipboard (`wl-clipboard`), settle / wait-for-stable, discrete-axis scroll
|
|
152
|
+
- [ ] Multi-monitor and fractional-scaling hardening
|
|
153
|
+
|
|
154
|
+
## Alternatives, honestly
|
|
155
|
+
|
|
156
|
+
| project | approach | where it falls short on Hyprland |
|
|
157
|
+
|---|---|---|
|
|
158
|
+
| computer-use-linux | AT-SPI + portals, ydotool fallback | GNOME-first; RemoteDesktop portal is dead on Hyprland, screenshots portal-first |
|
|
159
|
+
| hyprmcp | hyprctl wrapper | no screenshots, no input |
|
|
160
|
+
| wayland-mcp (×2) | setuid evemu + bundled VLM | archived / stale; evemu needs elevated setup |
|
|
161
|
+
| Anthropic computer-use-demo | X11 + xdotool in Docker | a sandboxed demo, not your desktop |
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
The launch and every release after it. Steps marked **[you]** need your
|
|
4
|
+
accounts/credentials; the rest is automated.
|
|
5
|
+
|
|
6
|
+
## One-time setup
|
|
7
|
+
|
|
8
|
+
1. **[you] Make the repo public** on GitHub (Settings → General → Danger
|
|
9
|
+
Zone). The awesome-list PRs and AUR source tarballs depend on this.
|
|
10
|
+
2. **[you] PyPI Trusted Publisher** — on PyPI, add a *pending* publisher so
|
|
11
|
+
the first `release` workflow run can publish without a token
|
|
12
|
+
(https://pypi.org/manage/account/publishing/):
|
|
13
|
+
- Project: `hypruse`
|
|
14
|
+
- Owner: `IlyasKhallouki` · Repo: `hypruse`
|
|
15
|
+
- Workflow: `release.yml` · Environment: `pypi`
|
|
16
|
+
3. **[you] AUR SSH key** — add your public key at
|
|
17
|
+
https://aur.archlinux.org/account, if not already done.
|
|
18
|
+
|
|
19
|
+
## Cutting a release
|
|
20
|
+
|
|
21
|
+
1. Bump `version` in `pyproject.toml` and move the CHANGELOG `[Unreleased]`
|
|
22
|
+
entries under the new version.
|
|
23
|
+
2. Tag and push:
|
|
24
|
+
```sh
|
|
25
|
+
git tag -a v0.1.0 -m "v0.1.0"
|
|
26
|
+
git push origin v0.1.0
|
|
27
|
+
```
|
|
28
|
+
The `release` workflow builds, publishes to PyPI via OIDC, and cuts a
|
|
29
|
+
GitHub release with the artifacts.
|
|
30
|
+
3. Verify: `uvx hypruse --version`.
|
|
31
|
+
|
|
32
|
+
## AUR
|
|
33
|
+
|
|
34
|
+
Stable (`packaging/aur/hypruse/`), after the tag exists:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
cd packaging/aur/hypruse
|
|
38
|
+
updpkgsums # fills the real sha256 for the tag tarball
|
|
39
|
+
makepkg --printsrcinfo > .SRCINFO
|
|
40
|
+
# push to the AUR remote:
|
|
41
|
+
# git clone ssh://aur@aur.archlinux.org/hypruse.git
|
|
42
|
+
# copy PKGBUILD + .SRCINFO in, commit, push
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`hypruse-git` never needs `updpkgsums` (VCS source); regenerate its
|
|
46
|
+
`.SRCINFO` the same way and push to `ssh://aur@aur.archlinux.org/hypruse-git.git`.
|
|
47
|
+
|
|
48
|
+
## Listings (after public + first release)
|
|
49
|
+
|
|
50
|
+
- `awesome-mcp-servers` and `awesome-hyprland` — PR one entry each.
|
|
51
|
+
- The MCP registry.
|
|
52
|
+
|
|
53
|
+
Test a PKGBUILD locally before pushing: `makepkg -si` in its directory.
|