list_windows
List all Hyprland windows: id (address), class, title, workspace, output, global geometry, floating/focus/visibility. Use the `id` for focus/screenshot.
无需输入参数。

com.smirnovlabs由 smirnovlabs.com 发布v0.2.0更新于 Nov 7, 2026
Hyprland/Wayland computer-use MCP: grim screenshots + uinput click/type + hyprctl targeting
发布方自己的服务:Ynab Mcp Server
Hyprland/Wayland 电脑操作服务器:截图、鼠标键盘输入、窗口控制,并提供 tmux 与 Chrome DevTools 通道。
Desktop only,通过 STDIO。 STDIO 服务会启动本地进程,因此需要 SourceWeft 桌面宿主。
参照 仓库 中的启动说明。
[Status: stable · maintained] [CI] [PyPI] [Python] [License: MIT]
A Hyprland/Wayland computer-use MCP server. It gives an MCP client (Claude Code, etc.) the ability to see your desktop, understand it via the Hyprland tree, and drive it with mouse + keyboard. It runs locally over stdio.
Why this exists: generic Linux computer-use servers are blind to the compositor —
they hand the model a giant multi-monitor screenshot and hope its pixel guesses
land. This server crops to one output (or one window) with grim, downscales to
the vision-API budget, and exposes hyprctl's tree so the model targets windows
by identity, not luck. Coordinates are always given in the screenshot the model
was just shown; the server maps them back to global layout pixels.
Built for Wayland/Hyprland: grim (capture) + a self-owned uinput device
(input) + hyprctl (tree/control) + wl-clipboard. It is the Wayland successor
to i3-computer-use-mcp (X11).
Window ids are Hyprland addresses (0x…) from list_windows — not integers.
See
Act
Control Hyprland (needs a Lua config, see Requirements)
Work off-screen (no window, no focus stolen)
The Hyprland tree uses global layout pixels spanning all monitors. Screenshots are cropped + downscaled, so every screenshot records a mapping frame (region origin + per-axis scale). Pointer tools accept coordinates in the screenshot's pixel space and translate them:
Practically: take a screenshot (it returns a frame_id), then call
click(x, y, frame_id=…) with the pixel you see in that image. Pointer tools
reject a stale or unknown frame_id rather than guessing against the wrong one.
Without a frame_id, they use your latest screenshot.
The uinput pointer positions absolutely: global pixels map to the device's abs
axes via the layout extent read from hyprctl monitors (verified ~1px).
hyprland.lua, Hyprland 0.55+) for the window-control tools.
These tools go through hyprctl eval and Hyprland's Lua hl.dispatch API:
focus_window, launch, goto_workspace, move_window_to_workspace,
set_floating, layout_message, run_dispatch, plus type_text/key with a
con_id and move_mouse(follow=True). Everything else works with any config:
screenshots, pointer and keyboard input, the window tree, the clipboard, and the
tmux/task/browser channels./dev/uinput for mouse and keyboard input (see below).uv.WAYLAND_DISPLAY,
XDG_RUNTIME_DIR and HYPRLAND_INSTANCE_SIGNATURE from the environment. MCP
clients launched from your desktop pass these through.The server shells out to a handful of CLI tools. screenshot needs grim, the
clipboard tools need wl-clipboard, and everything reads/drives Hyprland through
hyprctl; tmux and a Chromium/Chrome are only needed for the optional terminal
and browser channels.
Arch, in one go (Hyprland assumed already installed):
Input uses a self-owned uinput device, so the process needs read-write on
/dev/uinput. It needs no root and no ydotoold. Run getfacl /dev/uinput. If it
lists your user with rw (typically through a uaccess udev rule), you're set. If
not, add a rule that grants access to the user logged in at the seat:
The Python deps (mcp, pillow, evdev, websocket-client) are synced by uv
on first launch.
Miss one and the matching tool says so: calling screenshot without grim
returns "grim not found; install it …" straight to the model, so it can tell you
what to install instead of failing on a cryptic traceback.
It's a local stdio server. The client starts it on demand, and you don't need a clone.
Claude Code:
Clients with an mcpServers JSON config (Cursor, Gemini CLI, Cline, …):
Codex CLI (~/.codex/config.toml):
From a local checkout: the wrapper syncs deps on first launch. When a shared server is running, the wrapper connects to it.
Reconnect your client (in Claude Code: /mcp → reconnect) to pick up the server.
Smoke-test the plumbing first (read-only, no clicks):
By default each MCP client session starts its own hypr-cua. From a checkout you can
run one shared server per desktop login instead. A session then connects in
about 20 ms and costs about 17 MB instead of about 94 MB. bin/hypr-cua-mcp
connects to the shared server when it runs. When it doesn't, the wrapper starts a
stdio server for the session, so the registration stays the same.
Install the units. Run this in the checkout:
Start the socket from your Hyprland startup, so each new server gets the current
session's variables. In hyprland.lua, inside your startup handler:
Run the same command in a terminal once to start it without logging out.
Reconnect your MCP sessions (in Claude Code: /mcp → reconnect).
To upgrade, run git pull && uv sync && systemctl --user restart hypr-cua.service.
Open sessions reconnect by themselves. A call that was running during the restart
fails once with a message that it may have completed. The restart also ends
hypr-cua's tmux sessions and tasks.
Sessions share the server's browser. Its HYPR_CUA_CDP_* settings come from the
service's Environment= lines, not from the client. Screenshots stay per session:
the server rejects another session's frame_id as stale.
The cdp_* tools drive Chrome or Chromium tabs over the DevTools protocol, so
browser work needs no window focus. They never open your main profile.
The first cdp_* call picks a browser, and later calls keep it while it stays up:
cdp_targets reports which one is in use, "browser": "automation" or
"private". All sessions of one server share that browser, its tabs and its
cookies.
For logins that persist, start a dedicated automation profile on port 9222. From a
checkout, bin/hypr-cua-chrome does that. Otherwise run:
Log in only to what you're happy to automate. Any local process that can reach a DevTools port can drive that browser, and that includes the private one.
cdp_open and cdp_navigate wait for the page's load event, and for a CSS
selector if you pass wait_for. A page still loading at timeout (default 30 s)
comes back with "timed_out": true instead of an error. cdp_read takes a
selector and max_chars. cdp_screenshot returns an image of a tab's viewport.
Set them in your client's server config. In Claude Code:
claude mcp add hypr-cua -e HYPR_CUA_CDP_AUTOLAUNCH=0 -- uvx hypr-cua-mcp. In JSON
configs, add an "env": {…} block next to command.
follow_mouse=1 caveat: Hyprland keyboard focus tracks the pointer, so
focus_window(address) is best-effort — the reliable way to focus a window for
typing is to click it via screenshot coordinates. type_text(con_id=…)
raises rather than typing into an unverified window.docs/gotchas.md.type_text skips any other character. For anything
else, write_clipboard and paste.move_mouse(follow=True).cdp_click / cdp_type inject JavaScript, so pages that insist on trusted input
events won't react.Details, plus the task runner's edge cases:
KNOWN-ISSUES.md.
Stable and maintained. hypr-cua-mcp does what it set out to do, so the commit log is quiet on purpose. Expect bug fixes, Hyprland-compatibility updates and dependency bumps, not a stream of new features. A quiet repo here means done, not abandoned. Issues and PRs still get answered.
Bug reports and fixes are welcome; for anything bigger, open an issue first. See
CONTRIBUTING.md,
and AGENTS.md
for setup, tests, and the Hyprland conventions that bite.
On PyPI as hypr-cua-mcp and in the MCP
registry as com.smirnovlabs/hypr-cua-mcp (manifest:
server.json).
mcp-name: com.smirnovlabs/hypr-cua-mcp
MIT — see LICENSE.
来源:README.md,提交 fb8fa60
List all Hyprland windows: id (address), class, title, workspace, output, global geometry, floating/focus/visibility. Use the `id` for focus/screenshot.
无需输入参数。
List active outputs (monitors) with their global geometry, scale, workspace.
无需输入参数。
Screenshot one output (monitor); defaults to the focused output. Pass an output name from get_outputs (e.g. "DP-1") to target another monitor. Returns the image plus a frame_id — pass that frame_id to click/move/etc.
outputFull-resolution screenshot cropped to a single window (by address). NOTE: this crops the layout grab by the window's *geometry*. The window must be on its monitor's active workspace (else the region shows whatever IS there); a floating window sitting on top can also occlude it. A warning is added to the caption when the window isn't focused.
con_id必填Click at (x,y) in the pixels of screenshot `frame_id` (default: the last screenshot). button: left|middle|right. Errors if no screenshot exists yet or the frame_id is stale.
x必填y必填buttonframe_idDouble-click at (x,y) in screenshot `frame_id`'s pixel space.
x必填y必填buttonframe_idMove the pointer to (x,y) in screenshot `frame_id`'s pixel space. follow=True mimics a physical mouse's hover-focus: after moving, give keyboard focus to the window under the cursor. The synthetic pointer alone does NOT trigger Hyprland's own follow_mouse on a warp, so focus is set explicitly. No-op focus if the cursor lands on empty space or a bar. (A plain click already focuses what it hits — use follow to focus by hovering, without clicking.)
x必填y必填followframe_idDrag from (x1,y1) to (x2,y2), coords in screenshot `frame_id`'s pixel space.
x1必填x2必填y1必填y2必填buttonframe_idScroll at (x,y) (screenshot `frame_id` pixel space). direction: up|down.
x必填y必填amountframe_iddirectionGive keyboard focus to a window (by address). Raises if focus can't land.
con_id必填Type text into the focused window. If con_id (address) is given, focus it first and fail (no keystrokes sent) if that focus can't be verified. NOTE: US-ASCII only (uinput has no unicode-compose path); unmapped characters are skipped, and embedded newlines are Enter keypresses.
text必填con_idPress a key/chord (e.g. 'ctrl+s', 'Return', 'alt+Tab'). If con_id (address) is given, focus it first and fail if that focus can't be verified.
keys必填con_idCurrent global pointer location.
无需输入参数。
Read the text of a Wayland selection: 'clipboard' (Ctrl-C / Ctrl-Shift-V) or 'primary' (select / middle-click). Returns '' if empty.
selectionSet a Wayland selection ('clipboard' or 'primary') to text, then paste with key('ctrl+v' or 'ctrl+shift+v') or a middle-click. Returns only a length summary — never the value. secret=True marks the value for redaction in this server's action log (not built yet) and keeps it out of the return; the text still transits this tool call, so it appears in the MCP client's transcript.
text必填secretselectionList the copilot's tmux panes — session, pane, running command, size, state. They live on a dedicated 'hypr-cua' tmux socket, isolated from your real (default-socket) sessions. Detached = no window, no focus, zero screen footprint — the way to run terminal work on a non-visible workspace.
无需输入参数。
Create a detached copilot session `name` running a shell (or `command`). No window, no focus. Drive it with tmux_run/tmux_keys, read it with tmux_read.
name必填commandType a shell command line into session `target` and press Enter. Read the output afterward with tmux_read (the shell persists between calls).
target必填command必填Send interpreted tmux keys to `target` (e.g. 'C-c' to interrupt, 'Up', 'Escape', 'q') — for TUIs / control sequences, not literal command text.
keys必填target必填Capture session `target`'s terminal text (last `lines` incl. scrollback).
linestarget必填Kill copilot session `target` (only touches the isolated 'hypr-cua' socket).
target必填List the copilot browser's tabs as JSON {browser, tabs: [{id, title, url}]}. `browser` is "automation" (the Chrome on port 9222) or "private" (a headless Chrome hypr-cua started because nothing answered on 9222; it stops after 10 idle minutes, and its tabs and cookies go with it). Use a tab's `id` with the other cdp_* tools.
无需输入参数。
Open a new tab at `url` and wait for it to load, plus for CSS selector `wait_for` if given. Returns JSON {id, url, title, ready_state, waited_ms, timed_out}. A page still loading at `timeout` (1 to 300 s) returns timed_out=true instead of failing.
urltimeoutwait_forNavigate a tab (by id from cdp_targets) and wait like cdp_open. Returns the same JSON.
url必填target必填timeoutwait_forEvaluate a JavaScript expression in a tab and return its value as JSON. Promises are awaited. `timeout` (1 to 300 s) bounds the whole call. A thrown JS exception is an error.
target必填timeoutexpression必填Read the visible text (innerText) of a tab, or of the first element matching CSS `selector`, as JSON {title, url, text, chars_total, truncated}. The text is cut to max_chars inside the page.
target必填selectormax_charsScreenshot a tab's viewport in the copilot browser (no window focus needed). The image has no frame_id: act on the page with cdp_click or cdp_eval, not with the desktop pointer tools.
target必填Click the first element matching a CSS selector in a tab (by id). Errors if nothing matches the selector.
target必填selector必填Type text into the element matching a CSS selector in a tab (by id): sets its value and dispatches input/change events. Returns a length summary — not the text.
text必填target必填selector必填Close a tab (by id) in the copilot browser.
target必填Launch `command` as a background TASK in a hidden copilot tmux session — no window, no focus, zero screen footprint. The command is wrapped so completion is detectable (a `__TASK_DONE__:<exit_code>` sentinel), so you can poll it with task_status. Returns the task's initial {name, state} as JSON. Names must be unique and free of ':', '.', '/', and whitespace.
name必填command必填Check a task: JSON {name, state, exit_code?, tail}. state is 'running' (still going), 'done' (finished — `exit_code` 0 = success, non-zero = failure), or 'missing' (no such task). `tail` = that many last lines of its output.
name必填tailList all tasks with their state (running | done+exit_code) as JSON — the copilot tmux sessions started via task_start (plain tmux sessions excluded).
无需输入参数。
Send input to a running task — e.g. answer a prompt it's blocked on. Default types `text` + Enter; enter=False sends interpreted keys ('C-c', 'Up', 'Escape') for control sequences. (Detecting that a task is waiting on input is best-effort: read task_status's tail and answer accordingly.)
name必填text必填enterStop a task by killing its hidden tmux session (only touches the isolated 'hypr-cua' copilot socket).
name必填Launch an app via Hyprland's exec dispatcher. Opens on the current workspace — pair with goto_workspace or move_window_to_workspace to place it.
command必填Switch the focused monitor to a workspace (created if absent). Numeric names match by number; other names are matched literally.
workspace必填Move a window (by address) to a workspace without following it.
con_id必填workspace必填Enable/disable floating for a window (by address).
con_id必填enableSend a message to the active layout engine (dwindle/master), e.g. 'togglesplit', 'swapsplit', 'orientationnext'. (Hyprland has no per-window splith/tabbed/stacking like i3.)
message必填Escape hatch: run a raw Hyprland dispatcher as the Lua expression passed to hl.dispatch(...), e.g. 'hl.dsp.window.fullscreen({})' or 'hl.dsp.window.move({ direction = "l" })'. `exit` is blocked.
dsp_expr必填已满足适用检查项的 90%
该评分衡量此收录条目的完整程度,依据注册中心条目、端点与公开来源计算得出。它不是对服务器本身的评价,也不表示服务器安全或当前可用:可连通性是带日期的检查结果,评分也不包含安装量或用户评价。
请在注册中心条目中声明凭据请求头,或保持端点能够响应探测,以便判定其认证方式。
global_x = region.x + screenshot_x / scale_xglobal_y = region.y + screenshot_y / scale_ysudo pacman -S grim wl-clipboard uv tmux chromiumecho 'KERNEL=="uinput", SUBSYSTEM=="misc", TAG+="uaccess", OPTIONS+="static_node=uinput"' \ | sudo tee /etc/udev/rules.d/60-hypr-cua-uinput.rulessudo udevadm control --reload && sudo udevadm triggerclaude mcp add hypr-cua -- uvx hypr-cua-mcp{ "mcpServers": { "hypr-cua": { "command": "uvx", "args": ["hypr-cua-mcp"] } }}[mcp_servers.hypr-cua]command = "uvx"args = ["hypr-cua-mcp"]claude mcp add hypr-cua -- /path/to/hypr-computer-use-mcp/bin/hypr-cua-mcpuvx hypr-cua-mcp --selftestuv syncmkdir -p ~/.config/systemd/usercp contrib/systemd/hypr-cua.socket ~/.config/systemd/user/sed "s|@CHECKOUT@|$PWD|" contrib/systemd/hypr-cua.service > ~/.config/systemd/user/hypr-cua.servicesystemctl --user daemon-reloadhl.exec_cmd("systemctl --user import-environment DISPLAY WAYLAND_DISPLAY HYPRLAND_INSTANCE_SIGNATURE XDG_CURRENT_DESKTOP; systemctl --user stop hypr-cua.service; systemctl --user restart hypr-cua.socket")chromium --user-data-dir="$HOME/.local/share/hypr-cua-chrome" \ --remote-debugging-port=9222 --no-first-run --no-default-browser-check