Hyprland Computer Use

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

已验证STDIO仅桌面浏览器自动化开发者工具

概览

AI 生成的概览

Hyprland/Wayland 电脑操作服务器:截图、鼠标键盘输入、窗口控制,并提供 tmux 与 Chrome DevTools 通道。

可以做什么
  • 列出窗口与显示器,截取缩放后的显示器画面或全分辨率窗口裁剪图,读取指针位置
  • 点击、双击、拖拽、滚动、移动指针、输入文本和按键组合,坐标基于截图的像素空间
  • 聚焦、启动、浮动、移动窗口与切换工作区,并向布局引擎发送消息
  • 读写 Wayland 剪贴板与主选区
  • 运行独立的 tmux 会话和后台任务,发送命令或按键并读取输出
  • 通过 DevTools 协议操作 Chrome 标签页:打开、导航、执行 JavaScript、读取文本、截图、点击、输入和关闭
适用场景
适合让助手操作用户机器上真实的 Hyprland 桌面:点击图形界面应用、填写表单、读取屏幕文字,或在不抢占窗口焦点的情况下完成终端与浏览器工作。
运行要求
通过 uvx 以 stdio 在本地运行(需要 Python 3.11+ 和 uv)。需要 Hyprland Wayland 会话及 WAYLAND_DISPLAY、XDG_RUNTIME_DIR、HYPRLAND_INSTANCE_SIGNATURE,需要 hyprctl 和 grim,并对 /dev/uinput 有读写权限。剪贴板工具需要 wl-clipboard;tmux 与 Chromium/Chrome 仅用于可选的终端和浏览器通道。窗口控制工具需要 Lua 配置(hyprland.lua,Hyprland 0.55+)。可选 HYPR_CUA_CDP_* 变量用于配置浏览器通道。
安装前请注意
无身份验证也无沙箱:它操作真实桌面,能点击用户能点击的一切,包括带 sudo 的终端、密码管理器、邮件和聊天,因此实际运行时需有人监督。输入是真实的 uinput,且只能输入 US-ASCII 字符。剪贴板工具可读取敏感内容;write_clipboard 只返回长度。任何能访问 DevTools 端口的本地进程都能操控该浏览器,私有无头配置的标签页和 Cookie 在停止后即丢失。终止服务器即为紧急停止。

安装

在 SourceWeft 中

  1. 打开 控制台中的 Hyprland Computer Use,将其添加到工作区。
  2. 为需要使用其工具的对话启用该服务。

Desktop only,通过 STDIO。 STDIO 服务会启动本地进程,因此需要 SourceWeft 桌面宿主。

其他 MCP 客户端

参照 仓库 中的启动说明。

README

hypr-cua-mcp

[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).

Tools

Window ids are Hyprland addresses (0x…) from list_windows — not integers.

See

ToolWhat it does
list_windowsEvery Hyprland window: id (address), class, title, workspace, output, global rect, floating/focus/visibility
get_outputsActive monitors: global geometry, scale, workspace
screenshot(output?)Screenshot one output (default: focused), downscaled to the API budget
screenshot_window(con_id)Full-res crop of a single window (by address) — best for small text
get_mouseCurrent global pointer location

Act

ToolWhat it does
click / double_click / move_mouse / drag / scrollPointer actions; coords in a screenshot's pixel space (pass its frame_id). move_mouse(follow=True) also focuses the window under the pointer
type_text(text, con_id?) / key(keys, con_id?)Keyboard input; with a con_id, focus that window first and refuse to type if focus doesn't land
read_clipboard(selection?) / write_clipboard(text, selection?, secret?)Exact text in/out of the clipboard/primary selection (no OCR); write returns only a length, never the value

Control Hyprland (needs a Lua config, see Requirements)

ToolWhat it does
focus_window(con_id)Focus a window by address (best-effort; see the follow_mouse caveat under Safety)
launch(command)Launch an app via Hyprland's exec dispatcher
goto_workspace(ws) / move_window_to_workspace(con_id, ws)Switch to / move a window (by address) to a workspace
set_floating(con_id, enable?)Toggle a window's floating state
layout_message(message)Send a message to the active layout engine (e.g. togglesplit)
run_dispatch(dsp_expr)Raw Hyprland dispatcher escape hatch (exit blocked)

Work off-screen (no window, no focus stolen)

ToolWhat it does
tmux_list / tmux_new / tmux_run / tmux_keys / tmux_read / tmux_killDrive terminals in detached tmux sessions on an isolated hypr-cua socket; it never touches your own tmux sessions
task_start / task_status / task_list / task_send / task_stopRun a command as a background task and poll it (running → done + exit code); answer prompts with task_send
cdp_targets / cdp_open / cdp_navigate / cdp_read / cdp_eval / cdp_screenshot / cdp_click / cdp_type / cdp_closeDrive Chrome tabs over the DevTools protocol, never your main profile (see Browser channel)

The coordinate model (the important bit)

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:

global_x = region.x + screenshot_x / scale_xglobal_y = region.y + screenshot_y / scale_y

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).

Requirements

  • Hyprland on Wayland. Developed and tested on Hyprland 0.56 across three monitors at scale 1.
  • A Lua config (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.
  • Read-write access to /dev/uinput for mouse and keyboard input (see below).
  • Python 3.11+ and uv.
  • It runs inside your Hyprland session: it needs WAYLAND_DISPLAY, XDG_RUNTIME_DIR and HYPRLAND_INSTANCE_SIGNATURE from the environment. MCP clients launched from your desktop pass these through.

Install the dependencies

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.

ToolNeeded forArchDebian/Ubuntu
hyprctltree + control (required)bundled with hyprlandbundled with hyprland
grimscreenshot / screenshot_window (required)pacman -S grimapt install grim
wl-clipboardread_clipboard / write_clipboardpacman -S wl-clipboardapt install wl-clipboard
uvruns the server + syncs Python depspacman -S uvsee astral.sh/uv
tmuxtmux_* / task_* tools (optional)pacman -S tmuxapt install tmux
Chromium / Chromecdp_* browser tools (optional)pacman -S chromiumapt install chromium

Arch, in one go (Hyprland assumed already installed):

bash
sudo pacman -S grim wl-clipboard uv tmux chromium

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:

bash
echo '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 trigger

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.

Register with your MCP client

It's a local stdio server. The client starts it on demand, and you don't need a clone.

Claude Code:

bash
claude mcp add hypr-cua -- uvx hypr-cua-mcp

Clients with an mcpServers JSON config (Cursor, Gemini CLI, Cline, …):

json
{  "mcpServers": {    "hypr-cua": {      "command": "uvx",      "args": ["hypr-cua-mcp"]    }  }}

Codex CLI (~/.codex/config.toml):

toml
[mcp_servers.hypr-cua]command = "uvx"args = ["hypr-cua-mcp"]

From a local checkout: the wrapper syncs deps on first launch. When a shared server is running, the wrapper connects to it.

bash
claude mcp add hypr-cua -- /path/to/hypr-computer-use-mcp/bin/hypr-cua-mcp

Reconnect your client (in Claude Code: /mcp → reconnect) to pick up the server. Smoke-test the plumbing first (read-only, no clicks):

bash
uvx hypr-cua-mcp --selftest

One shared server per login

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.

  1. Install the units. Run this in the checkout:

    bash
    uv 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-reload
  2. Start the socket from your Hyprland startup, so each new server gets the current session's variables. In hyprland.lua, inside your startup handler:

    lua
    hl.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")

    Run the same command in a terminal once to start it without logging out.

  3. 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.

Browser channel

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:

  1. If a browser answers on port 9222, the tools attach to it. Run your automation profile there to keep logins.
  2. Otherwise they start a private headless Chrome with a throwaway profile. Chrome picks a free port, so it never collides with 9222. It stops after 10 minutes without a call, and when the server exits.

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:

bash
chromium --user-data-dir="$HOME/.local/share/hypr-cua-chrome" \  --remote-debugging-port=9222 --no-first-run --no-default-browser-check

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.

VariableDefaultEffect
HYPR_CUA_CDP_PORT9222Port of the automation browser to attach to
HYPR_CUA_CDP_AUTOLAUNCH10 = attach-only: never start a private Chrome, and fail if nothing answers on the port
HYPR_CUA_CDP_BINfirst Chrome/Chromium on PATHBrowser binary for the private Chrome
HYPR_CUA_CDP_USER_DATA_DIR~/.local/share/hypr-cua-chromeProfile directory for bin/hypr-cua-chrome

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.

Safety

  • No auth — it's a local stdio server. Don't expose it over a socket.
  • There is no sandbox on Wayland: it drives your real desktop and can click anything you can — terminals with sudo, your password manager, Slack, email. Supervise live runs.
  • Emergency stop: kill the MCP server (its uinput device dies with it).
  • 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.
  • Input is real uinput, so it resets the compositor idle timer naturally (no synthetic-input self-lock, unlike X11). See docs/gotchas.md.

Known limitations

  • Typing is US-ASCII only: type_text skips any other character. For anything else, write_clipboard and paste.
  • Capture on monitors with scale ≠ 1 is untested, and the crop is likely off there.
  • The synthetic pointer doesn't hover-focus. Click to focus, or use 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.

Project status

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.

Contributing

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.

Registry

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

License

MIT — see LICENSE.

来源:README.md,提交 fb8fa60

工具

41

list_windows

List all Hyprland windows: id (address), class, title, workspace, output, global geometry, floating/focus/visibility. Use the `id` for focus/screenshot.

无需输入参数。

get_outputs

List active outputs (monitors) with their global geometry, scale, workspace.

无需输入参数。

screenshot

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.

输入参数output

screenshot_window

Full-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

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_id

double_click

Double-click at (x,y) in screenshot `frame_id`'s pixel space.

输入参数x必填y必填buttonframe_id

move_mouse

Move 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_id

drag

Drag from (x1,y1) to (x2,y2), coords in screenshot `frame_id`'s pixel space.

输入参数x1必填x2必填y1必填y2必填buttonframe_id

scroll

Scroll at (x,y) (screenshot `frame_id` pixel space). direction: up|down.

输入参数x必填y必填amountframe_iddirection

focus_window

Give keyboard focus to a window (by address). Raises if focus can't land.

输入参数con_id必填

type_text

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_id

key

Press 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_id

get_mouse

Current global pointer location.

无需输入参数。

read_clipboard

Read the text of a Wayland selection: 'clipboard' (Ctrl-C / Ctrl-Shift-V) or 'primary' (select / middle-click). Returns '' if empty.

输入参数selection

write_clipboard

Set 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必填secretselection

tmux_list

List 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.

无需输入参数。

tmux_new

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必填command

tmux_run

Type a shell command line into session `target` and press Enter. Read the output afterward with tmux_read (the shell persists between calls).

输入参数target必填command必填

tmux_keys

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必填

tmux_read

Capture session `target`'s terminal text (last `lines` incl. scrollback).

输入参数linestarget必填

tmux_kill

Kill copilot session `target` (only touches the isolated 'hypr-cua' socket).

输入参数target必填

cdp_targets

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.

无需输入参数。

cdp_open

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_for

cdp_navigate

Navigate a tab (by id from cdp_targets) and wait like cdp_open. Returns the same JSON.

输入参数url必填target必填timeoutwait_for

cdp_eval

Evaluate 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必填

cdp_read

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_chars

cdp_screenshot

Screenshot 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必填

cdp_click

Click the first element matching a CSS selector in a tab (by id). Errors if nothing matches the selector.

输入参数target必填selector必填

cdp_type

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必填

cdp_close

Close a tab (by id) in the copilot browser.

输入参数target必填

task_start

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必填

task_status

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必填tail

task_list

List all tasks with their state (running | done+exit_code) as JSON — the copilot tmux sessions started via task_start (plain tmux sessions excluded).

无需输入参数。

task_send

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必填enter

task_stop

Stop a task by killing its hidden tmux session (only touches the isolated 'hypr-cua' copilot socket).

输入参数name必填

launch

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必填

goto_workspace

Switch the focused monitor to a workspace (created if absent). Numeric names match by number; other names are matched literally.

输入参数workspace必填

move_window_to_workspace

Move a window (by address) to a workspace without following it.

输入参数con_id必填workspace必填

set_floating

Enable/disable floating for a window (by address).

输入参数con_id必填enable

layout_message

Send 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必填

run_dispatch

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必填

版本历史

3
  1. v0.2.0最新Nov 7, 2026
  2. v0.1.2Oct 17, 2026
  3. v0.1.1Sep 16, 2026

评分

已满足适用检查项的 90%

该评分衡量此收录条目的完整程度,依据注册中心条目、端点与公开来源计算得出。它不是对服务器本身的评价,也不表示服务器安全或当前可用:可连通性是带日期的检查结果,评分也不包含安装量或用户评价。

  • 近 30 天内可连通25 分
  • 已声明认证方式10 分

    请在注册中心条目中声明凭据请求头,或保持端点能够响应探测,以便判定其认证方式。

  • 已列出工具必需15 分
  • 描述必需15 分
  • 图标10 分
  • 文档15 分
  • 许可证10 分

其他 Smirnovlabs 集成