niri-computer-use0.1.1MCP server for niri 26.04 on Linux

Your agent can use your niri desktop. One key takes it back.

An MCP server for Claude Code, Codex, Pi and other MCP clients. The agent reads your windows and screenshots; while it holds the lease, it clicks, types and runs niri actions.

Figure 1. Five real sessions: each recording, and under it the calls it made.

Session
Stop key. You press the stop key between two calls. The lease goes back to you, and the agent’s next click comes back stopped.

Asked “Go through the niri releases open in the browser, one by one from v26.04 back to v25.01, and note the biggest change in each into NOTES.txt.”

CallsLeaseniri sawYou
8 calls in 12 s.
All 8 calls, as text
  1. → desktop_state← focused_window: 17 · org.gnome.Nautilus 11 · helium 12 · ncu-demo-agent 17 · 0 ms
  2. → acquire_desktop← lease taken · users_window: 17 · 2 ms
  3. → focus {"window_id": 12, "screenshot": true}← accepted: true · observed: focused · focus 17 → 12 · 550 ms
  4. → elements {"window_id": 12, "role": "heading", "limit": 60}← refused: not_accessible · 0 ms
  5. → key {"keys": ["ctrl+minus", "ctrl+minus", "ctrl+minus"], "expect": {"window_id": 12}, "screenshot": true}← accepted: true · observed: sent · 267 ms
  6. → scroll {"screenshot_ref": "shot-ac2ffeab-164", "x": 1000, "y": 500, "notches_y": 10, "screenshot": true}← accepted: true · observed: sent · 452 ms
  7. $ niri-computer-use stop# the command the stop key runs; it takes the lease back
  8. → click {"screenshot_ref": "shot-ac2ffeab-168", "x": 700, "y": 117, "screenshot": true}← refused: stopped · 0 ms
Focus moved. The agent types a paragraph into nvim, then you move focus to Files. Its next key was meant for nvim, so it is refused.

Asked “In the nvim window, add a paragraph under the title saying what niri-computer-use 0.1.0 does, about three sentences. Type it, don't save.”

CallsLeaseniri sawYou
8 calls in 12 s.
All 8 calls, as text
  1. → desktop_state← focused_window: 20 · org.gnome.Nautilus 11 · helium 12 · ncu-demo-notes 19 · ncu-demo-agent 20 · 0 ms
  2. → acquire_desktop← lease taken · users_window: 20 · 2 ms
  3. → focus {"window_id": 19, "screenshot": true}← accepted: true · observed: focused · focus 20 → 19 · 557 ms
  4. → key {"keys": ["Escape", "o", "Return"], "expect": {"window_id": 19}}← accepted: true · observed: sent · 254 ms
  5. → type_text {"text": "niri-computer-use 0.1.0 is an MCP server that lets an AI agent see and operate a niri Wayland desktop. It reads windows, workspaces and accessible elements, takes screenshots, and, while holding an exclusive desktop lease, can focus windows, click, type and run niri actions. The user stays in control with a stop key, a policy file that limits what the agent may do, and an audit log of every action.", "expect": {"window_id": 19}, "screenshot": true}← accepted: true · observed: sent · 2482 ms
  6. $ niri msg action focus-window --id 11# you move focus to Files; the agent never asked for it
  7. → key {"keys": ["Escape"], "expect": {"window_id": 19}}← refused: focus_mismatch · 2 ms
  8. → release_desktop {"restore_focus": true}← released · focus back on 20 · observed: focused · 4 ms
Errand. The agent selects two screenshots in Files, makes a folder for them from the right-click menu, names it release-0.1 and gives focus back.

Asked “In Files, put the two screenshots into a new folder called release-0.1, then bring me back here.”

CallsLeaseniri sawYou
11 calls in 18 s.
All 11 calls, as text
  1. → status← ok · 3 ms
  2. → desktop_state← focused_window: 15 · org.gnome.Nautilus 11 · helium 12 · ncu-demo-agent 15 · 3 ms
  3. → acquire_desktop← lease taken · users_window: 15 · 2 ms
  4. → focus {"window_id": 11, "screenshot": true}← accepted: true · observed: focused · focus 15 → 11 · 858 ms
  5. → click {"screenshot_ref": "shot-ac2ffeab-43", "x": 979, "y": 88}← accepted: true · observed: sent · 252 ms
  6. → key {"keys": ["shift+Right", "ctrl+alt+n"], "expect": {"window_id": 11}, "screenshot": true}← accepted: true · observed: sent · 469 ms
  7. → click {"screenshot_ref": "shot-ac2ffeab-47", "x": 1061, "y": 88, "button": "right", "screenshot": true}← accepted: true · observed: sent · 957 ms
  8. → click {"screenshot_ref": "shot-ac2ffeab-56", "x": 1130, "y": 100, "screenshot": true}← accepted: true · observed: sent · 856 ms
  9. → key {"keys": ["ctrl+a"], "expect": {"app_id": "org.gnome.Nautilus"}}← accepted: true · observed: sent · 253 ms
  10. → type_text {"text": "release-0.1", "expect": {"app_id": "org.gnome.Nautilus"}, "submit": true, "screenshot": true}← accepted: true · observed: sent · 920 ms
  11. → release_desktop {"restore_focus": true}← released · focus back on 15 · observed: focused · 3 ms
Layout. The agent sizes Files and the browser to a third and two thirds, then floats its own terminal small in the corner, all with niri’s own actions.

Asked “Make room for the browser: Files a third of the screen wide, the browser the other two thirds, and float this terminal small in the bottom right corner.”

CallsLeaseniri sawYou
10 calls in 16 s.
All 10 calls, as text
  1. → desktop_state← focused_window: 14 · org.gnome.Nautilus 11 · helium 12 · ncu-demo-agent 14 · 0 ms
  2. → acquire_desktop← lease taken · users_window: 14 · 3 ms
  3. → niri_action {"action": {"SetWindowWidth": {"id": 11, "change": {"SetProportion": 33.333}}}}← accepted: true · observed: changed · window 11 size 839×1374 · 206 ms
  4. → niri_action {"action": {"SetWindowWidth": {"id": 12, "change": {"SetProportion": 66.667}}}}← accepted: true · observed: changed · window 12 size 1690×1374 · 217 ms
  5. → niri_action {"action": {"MoveWindowToFloating": {"id": 14}}}← accepted: true · observed: changed · 203 ms
  6. → niri_action {"action": {"SetWindowWidth": {"id": 14, "change": {"SetFixed": 800}}}}← accepted: true · observed: changed · window 14 size 800×1374 · 206 ms
  7. → niri_action {"action": {"SetWindowHeight": {"id": 14, "change": {"SetFixed": 500}}}}← accepted: true · observed: changed · window 14 size 800×500 · 206 ms
  8. → niri_action {"action": {"MoveFloatingWindow": {"id": 14, "x": {"SetFixed": 1740}, "y": {"SetFixed": 920}}}, "screenshot": true}← accepted: true · observed: changed · 1706 ms
  9. → niri_action {"action": {"MoveFloatingWindow": {"id": 14, "x": {"SetFixed": 1740}, "y": {"SetFixed": 874}}}, "screenshot": true}← accepted: true · observed: changed · 1263 ms
  10. → release_desktop {"restore_focus": true}← released · focus back on 14 · observed: focused · 2 ms
Noctalia. The agent opens Noctalia’s control center, turns on Do Not Disturb and closes the panel.

Asked “Open Noctalia's control center and turn on Do Not Disturb there, then close it.”

CallsLeaseniri sawYou
6 calls in 10 s.
All 6 calls, as text
  1. → status← ok · 1 ms
  2. → acquire_desktop← lease taken · users_window: 18 · 2 ms
  3. → shell_panel {"panel": "control-center", "open": true, "screenshot": true}← accepted: true · observed: opened · focus 18 → None · 1514 ms
  4. → click {"screenshot_ref": "shot-ac2ffeab-187", "x": 711, "y": 215, "screenshot": true}← accepted: true · observed: sent · 1064 ms
  5. → shell_panel {"panel": "control-center", "open": false}← accepted: true · observed: closed · focus None → 18 · 391 ms
  6. → release_desktop {"restore_focus": true}← released · focus back on 18 · observed: focused · 2 ms

Recorded on a real niri 26.04 desktop with Claude Code (Opus 5.5) and the installed server; nothing was scripted. Thinking time between calls is cut to 1.2 s; the calls run in real time.

What an action answers

Each action says whether niri, Noctalia or the app took the request, what happened afterwards, and what niri saw change on the desktop.

{
  "accepted": true,
  "observed": "one",
  "windows": [4],
  "focused_window": 4,
  "changes": {
    "focus": {"from": 3, "to": 4},
    "opened": [{"app_id": "notes", "id": 4,
                "title": "harness window", "workspace_id": 1}]
  }
}
What launch {"preset": "notes"} answered in the README’s example run, in a headless nested niri. Every call and the grading.
accepted
Whether niri, Noctalia or, for an element action, the app took the request. False when nothing was sent, null when the request went out but the reply was lost.
observed
What happened afterwards, such as focused, opened or closed in niri’s event stream; for input, sent once niri has handled it; for set_element_text, matched or differs by the app’s own count.
changes
What niri saw change: windows opened and closed, focus and workspace moving, a title, size or floating state. Left out when niri saw nothing.
error
When an action is refused or fails, in place of the above: a stable name such as focus_mismatch, and a detail that keeps niri’s, Noctalia’s or the program’s own message. An app’s own messages from element actions are left out, since they can hold a field’s text. Wrong arguments get a plain-text error instead.
Every tool and its result

Before anything reaches niri

Every action passes the first three checks, in this order; the last two belong to the tools. The first check that fails answers the agent, and nothing goes out.

AgentClaude Code, Codex, Pi
MCP, stdio

niri-computer-use

  1. Stop flagset by your stop key
  2. Lease
  3. Lock state
  4. Focus
  5. Deny list

Every call that reaches a tool is written to the audit log.

IPC, input
niriits IPC and event stream, virtual pointer and keyboard, AT-SPI, Noctalia
Pick a row below to send an action through the checks.
Table 1. When an action is refused. The details are the server’s own messages; the app_id and window ids are examples. Two more checks sit between these, for stuck input and for niri’s version; Safety lists them all.
CheckRefused whenApplies toThe agent gets
You pressed the stop key.every actionstoppedthe stop flag is set; the user clears it with niri-computer-use resume
This agent didn’t take the lease, or another one holds it.every actionlease_requiredthis session doesn't hold the lease; call acquire_desktop first
The screen is locked, or neither logind nor Noctalia says whether it is.every actionscreen_lockedthe screen is locked
The window the agent named in expect, or an element action’s window, doesn’t have focus. Typing checks again before each key.typing, keys and pastes, checked first; element actions, checked after the deny listfocus_mismatchexpected window_id 19; window 11 with app_id Some("org.gnome.Nautilus")
An app on your deny list has focus, or owns the window an element action or a close aims at.pointer, keyboard and element actions, and closing a windowapp_deniedthe focused window's app_id "org.keepassxc.KeePassXC" is on the policy's deny list
Nothing above applies.every actionaccepted: true · observed: sent, for a type_text call

Guardrails, not a sandbox

What it enforces

  • One agent at a time holds the lease, and only that agent acts. Looking needs no lease, except reading the clipboard.
  • The stop key cancels the running action and takes the lease back. An agent can’t press it.
  • With confirm_lease on, you allow each lease from a desktop notification. It is off by default.
  • Element actions refuse fields the toolkit marks as password fields.
  • The launch tool starts only presets you write, and niri actions that run programs need unrestricted on.
  • An audit log of every call that reaches a tool, without typed text, the clipboard, window titles or images. A failed write shows in status.

What it doesn’t stop

  • Any connected agent sees your screen and window titles.
  • A click can land on a denied window that doesn’t have focus.
  • Typing and clicking don’t check for password fields.
  • A lock that comes after the last read isn’t caught.
  • An agent that can run shell commands can answer the confirm_lease notification itself.
  • With the lease, a click or a key can do anything you could do.
Read Safety before letting an agent act

What it needs

niri26.04. A 26.04 patch release works with a warning; any other release is read-only.
SystemLinux on x86_64 or aarch64. Release builds need glibc 2.39 or newer; cargo needs Rust 1.99.
At run timegrim, loginctl and libxkbcommon; notify-send if you turn on confirm_lease.
AgentsClaude Code, Codex, Pi or any MCP client that starts a server over stdio.
Pointer toolsOne monitor at transform Normal. The keyboard, screenshots and the rest work with any.
ElementsGTK 4 works. GTK 3 and Qt apps that draw their own title bar give no element boxes.
NoctaliaOptional. A session without Noctalia or an accessibility bus has 17 of the 22 tools.

Install

Three steps, from a shell inside your niri session.

  1. Download the install script, read it, run it. It installs the binary and the skill that tells your agents how to use the server, then offers to register the server in each of Claude Code, Pi and Codex it finds. What the script does

    curl -fsSLO https://github.com/ayagmar/niri-computer-use/releases/latest/download/install.sh
    less install.sh
    sh install.sh

    Or build the crate with cargo; then add the skill. The skill

    cargo install --locked niri-computer-use
  2. If you said no, or built with cargo, register it yourself, here in Claude Code with the binary’s path (~/.cargo/bin after cargo). Codex, Pi and other clients

    claude mcp add --scope user niri-computer-use -- ~/.local/bin/niri-computer-use serve
  3. Bind the stop key in niri’s config.kdl.

    Mod+Shift+Escape allow-inhibiting=false allow-when-locked=true { spawn "/home/you/.local/bin/niri-computer-use" "stop"; }
Full install guide