Hyprland Computer Use

com.smirnovlabsPublished by smirnovlabs.comv0.2.0Updated Nov 7, 2026

Hyprland/Wayland computer-use MCP: grim screenshots + uinput click/type + hyprctl targeting

The publisher's own server: Ynab Mcp Server

VerifiedSTDIODesktop onlyBrowser AutomationDeveloper Tools

Overview

AI-generated overview

Hyprland/Wayland computer-use server: screenshots, mouse and keyboard input, window control, plus tmux and Chrome DevTools channels.

What you can do
  • List windows and monitors, take downscaled monitor screenshots or full-resolution window crops, and read the pointer position
  • Click, double-click, drag, scroll, move the pointer, type text and press key chords, with coordinates in a screenshot's pixel space
  • Focus, launch, float, move and switch windows and workspaces, and send layout-engine messages
  • Read and write the Wayland clipboard and primary selection
  • Run detached tmux sessions and background tasks, sending commands or keys and reading their output
  • Drive Chrome tabs over the DevTools protocol: open, navigate, evaluate JavaScript, read text, screenshot, click, type and close
When to use it
Useful when an assistant should operate a real Hyprland desktop on the user's machine: clicking through GUI apps, filling forms, reading on-screen text, or running terminal and browser work without stealing window focus.
Requirements
Runs locally over stdio via uvx (Python 3.11+ and uv). Needs a Hyprland Wayland session with WAYLAND_DISPLAY, XDG_RUNTIME_DIR and HYPRLAND_INSTANCE_SIGNATURE, hyprctl and grim, and read-write access to /dev/uinput. wl-clipboard is needed for clipboard tools; tmux and Chromium/Chrome only for the optional terminal and browser channels. Window-control tools need a Lua config (hyprland.lua, Hyprland 0.55+). Optional HYPR_CUA_CDP_* variables configure the browser channel.
Before you install
No authentication and no sandbox: it drives the real desktop and can click anything the user can, including terminals with sudo, password managers, email and chat, so live runs should be supervised. Input is real uinput, and typing is US-ASCII only. The clipboard tools can read secrets; write_clipboard returns only a length. Any local process that can reach the DevTools port can drive that browser, and the private headless profile's tabs and cookies are discarded when it stops. Killing the…

Installation

In SourceWeft

  1. Open Hyprland Computer Use in the dashboard and add it to a workspace.
  2. Enable the server for the chats that should use its tools.

Desktop only via STDIO. STDIO servers start a local process, so they need the SourceWeft desktop host.

Other MCP clients

Follow the launch instructions in the repository.

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.

Source: README.md at commit fb8fa60

Tools

41

list_windows

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

Takes no parameters.

get_outputs

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

Takes no parameters.

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.

Parametersoutput

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.

Parameterscon_idrequired

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.

Parametersxrequiredyrequiredbuttonframe_id

double_click

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

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

Parametersxrequiredyrequiredfollowframe_id

drag

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

Parametersx1requiredx2requiredy1requiredy2requiredbuttonframe_id

scroll

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

Parametersxrequiredyrequiredamountframe_iddirection

focus_window

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

Parameterscon_idrequired

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.

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

Parameterskeysrequiredcon_id

get_mouse

Current global pointer location.

Takes no parameters.

read_clipboard

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

Parametersselection

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.

Parameterstextrequiredsecretselection

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.

Takes no parameters.

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.

Parametersnamerequiredcommand

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

Parameterstargetrequiredcommandrequired

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.

Parameterskeysrequiredtargetrequired

tmux_read

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

Parameterslinestargetrequired

tmux_kill

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

Parameterstargetrequired

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.

Takes no parameters.

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.

Parametersurltimeoutwait_for

cdp_navigate

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

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

Parameterstargetrequiredtimeoutexpressionrequired

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.

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

Parameterstargetrequired

cdp_click

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

Parameterstargetrequiredselectorrequired

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.

Parameterstextrequiredtargetrequiredselectorrequired

cdp_close

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

Parameterstargetrequired

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.

Parametersnamerequiredcommandrequired

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.

Parametersnamerequiredtail

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

Takes no parameters.

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

Parametersnamerequiredtextrequiredenter

task_stop

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

Parametersnamerequired

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.

Parameterscommandrequired

goto_workspace

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

Parametersworkspacerequired

move_window_to_workspace

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

Parameterscon_idrequiredworkspacerequired

set_floating

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

Parameterscon_idrequiredenable

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

Parametersmessagerequired

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.

Parametersdsp_exprrequired

Version history

3
  1. v0.2.0LatestNov 7, 2026
  2. v0.1.2Oct 17, 2026
  3. v0.1.1Sep 16, 2026

Score

90% of the applicable checks met

How complete this listing is, computed from the registry entry, the endpoint and public sources. Not a rating of the server and not a statement that it is safe or working today: reachability is a dated check, and no installs or reviews go into the score.

  • Reached in the last 30 days25 pts
  • Auth declared10 pts

    Declare the credential headers in the registry entry, or keep the endpoint answering the probe so its auth can be classified.

  • Tools listedRequired15 pts
  • DescriptionRequired15 pts
  • Logo10 pts
  • Documentation15 pts
  • License10 pts

Other Smirnovlabs integrations