Skip to main content
Glama

dibs

A local hub for AI Agent Computer Use. Let your AI agents call dibs on using your desktop.

It lets multiple agents share one Windows desktop by exposing screenshot, mouse, keyboard, and window control over HTTP and MCP, so a Claude Code session, a scheduled automation task, and a browsing agent can all drive the same machine without fighting each other — or without shoving the user out of their own chair.

The mental model: agents register, one has dibs on "the desk" at a time (an exclusive lease on the mouse/keyboard), the human always wins, every action is logged, there's one big pause button, and there's an on-screen overlay so anyone glancing at the monitor can see which agent is doing what.

Install

uv venv --python 3.12
uv sync

That creates .venv and installs everything (FastAPI, uvicorn, mss, pyautogui, pywin32, pynput, the mcp SDK, httpx, uiautomation). No extra dependencies to add yourself.

Related MCP server: windows-computer-use-mcp

Run

uv run dibs serve

Prints the dashboard URL and the admin token, then serves REST (/v1/*), MCP (/mcp), and the dashboard (/) all on one port (7474 by default).

For it to start automatically at Windows logon:

.\scripts\install-task.ps1

To have it restarted automatically if it ever stops answering (the task only restarts on a crash it can see):

powershell -ExecutionPolicy Bypass -File scripts\install-watchdog.ps1

That registers dibs-watchdog, which hits the unauthenticated GET /healthz every 5 minutes and starts the dibs task when it fails. scripts\uninstall-task.ps1 removes both tasks.

On this machine Register-ScheduledTask needs an elevated shell (UAC-filtered admin token): open Windows Terminal as administrator and run the installer from there. The task itself runs unelevated as your interactive user. The installer stops any hand-started dibs serve first so the task can take port 7474.

This registers a Scheduled Task named dibs, not a Windows service — a service runs in session 0, which can't see the desktop or send input at all. The task runs in your interactive logon session with a hidden window instead. .\scripts\uninstall-task.ps1 removes it.

First run

uv run dibs serve prints an admin token the first time it runs (stored once in data/secrets.json). You'll need it for anything that isn't loopback-open. Grab it again anytime: uv run dibs token.

Dashboard

http://127.0.0.1:7474 — a single-page, no-build-step dashboard:

  • Live screenshot (polls every second) with a screen picker for multi-monitor setups.

  • Mode selector (Ask me / Hands-off / Locked) and a human-presence chip.

  • A consent card that appears whenever an agent is waiting on you, with a countdown and Allow / Deny buttons.

  • The desk card: who holds it, the wait queue, recent consent decisions, force-release and take-the-desk-back buttons.

  • Agents table (revoke a token), stats, and an audit tail with screenshot thumbnails.

It asks for the admin token the first time it hits a 401 and remembers it in the browser. Looks fine on a phone.

Register an agent

uv run dibs register --name my-agent --purpose "testing things"

Prints the new agent's token once — store it. Equivalent over curl (works without a token from loopback if allow_local_open_registration is true, the default):

curl -s -X POST http://127.0.0.1:7474/v1/agents \
  -H "Content-Type: application/json" \
  -d '{"name":"my-agent","purpose":"testing things"}'

Use it — REST

Every action except wait needs dibs first, screenshots included: nobody looks at your screen without you saying yes.

TOKEN=<agent token>

# acquire the desk (long-polls up to wait_s if someone else holds it)
curl -s -X POST http://127.0.0.1:7474/v1/lease \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"ttl_s":60,"wait_s":10}'

# screenshot (needs dibs like everything else)
curl -s -o shot.png "http://127.0.0.1:7474/v1/screenshot.png?screen=0" \
  -H "Authorization: Bearer $TOKEN"

# click and type
curl -s -X POST http://127.0.0.1:7474/v1/actions \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"action":"left_click","coordinate":[400,300]}'
curl -s -X POST http://127.0.0.1:7474/v1/actions \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"action":"type","text":"hello"}'

# scroll_pages -- more reliable than `scroll` against pages/apps that smooth-scroll (Chrome
# can collapse a burst of wheel events into a much smaller move than requested); clicks
# coordinate first if given, then sends Page_Up/Page_Down `scroll_amount` times
curl -s -X POST http://127.0.0.1:7474/v1/actions \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"action":"scroll_pages","scroll_direction":"down","scroll_amount":2,"coordinate":[900,700]}'

# batch (stops at first failure)
curl -s -X POST http://127.0.0.1:7474/v1/actions/batch \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"actions":[{"action":"left_click","coordinate":[400,300]},{"action":"type","text":"hi"}],"auto_lease":true}'

# find + click a labeled element instead of guessing pixel coordinates
curl -s -X POST http://127.0.0.1:7474/v1/actions \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"action":"find","text":"Add to cart","near":"Blue Widget"}'
curl -s -X POST http://127.0.0.1:7474/v1/actions \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"action":"click_element","text":"Add to cart","near":"Blue Widget"}'

# screenshot_after: opt in on any single action or batch to get a screenshot back with the
# result, taken right after the action ran (or after the last successful action of a batch,
# even one that stopped early on error) -- same shape as the `screenshot` action's own image
curl -s -X POST http://127.0.0.1:7474/v1/actions \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"action":"key","text":"Return","screenshot_after":true}'
# -> {"ok":true,"result":"OK","screenshot":{"png_base64":"...","width":...,"height":...,"scale":...,"screen":0}}
curl -s -X POST http://127.0.0.1:7474/v1/actions/batch \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"actions":[{"action":"left_click","coordinate":[400,300]}],"auto_lease":true,"screenshot_after":true}'
# -> {"results":[...],"screenshot":{...}}

POST /v1/lease responds one of four ways depending on mode and who's around:

  • granted (200) — you hold it.

  • queued (202) — someone else holds it; wait_s ran out, keep polling.

  • awaiting_consent (202) — mode is ask and a human is active; a consent prompt is up (overlay + dashboard + hotkeys), keep polling with the same request.

  • denied (403) — locked mode, human said no, or the request timed out; check reason and retry_after_s.

Use it — MCP from Claude Code

claude mcp add --transport http dibs http://127.0.0.1:7474/mcp \
  --header "Authorization: Bearer <token>"

Exposes nine tools: computer (one tool, action param selects the behavior — mirrors Anthropic's computer_toolset_20260801, plus dibs extras), desk_status, acquire_desk, release_desk, list_windows, focus_window, ui_tree, find, click_element. computer always auto-leases, and a denial comes back as a tool error naming the holder, the consent countdown, or when it'll auto-resume — so the model can decide whether to wait or back off.

UI Automation: ui_tree / find / click_element

Reading a full-screen screenshot and guessing pixel coordinates is expensive and fragile — a scrolled page can move the target out from under a hard-coded click. These three actions read the real Windows UI Automation tree instead, so an agent can say "click the element named 'Add to cart' nearest to 'Blue Widget'" and get it right even after a scroll:

  • ui_tree(hwnd?, title?, max_depth?=6, max_nodes?=400, roles?) — a compact indented text tree plus structured nodes ({id, role, name, value?, rect, rect_shot, depth, enabled, offscreen}). rect is absolute screen pixels; rect_shot is screenshot-space pixels (the same coordinate system screenshot/zoom/clicks use), so a returned rect can be clicked directly.

  • find(text, role?, near?, hwnd?, title?, exact?) — case-insensitive substring match on name/value, optional role filter, near picks the match closest to a named landmark when several elements share a name. Returns the best match plus up to 5 alternates (center/center_shot included), or a not_found error listing nearby element names.

  • click_element(text, role?, near?, hwnd?, title?, exact?, button?, double?)find, then click the centre. Goes through the same click path as left_click, so motion, the overlay flash, and the audit log all work the same way.

Window resolution for all three: hwnd if given, else a case-insensitive substring of title, else the current foreground window.

Chromium/Electron apps (Chrome, VS Code, Slack, the Edge WebView...) only publish their accessibility tree once an assistive-tech client connects. uiautomation usually triggers that on the first query, but sometimes only a beat later — dibs retries a couple of times with a short pause before giving up. If a Chromium-based app still comes back nearly empty, launch it with --force-renderer-accessibility to force the tree on from startup.

Use it — Python client

clients/python/dibs_client.py is a small sync httpx wrapper, no dependency on the dibs package itself:

from dibs_client import DibsClient

client = DibsClient("http://127.0.0.1:7474", token="<agent token>")
client.acquire(ttl_s=60)
client.click(400, 300)
png, w, h, scale = client.screenshot()
client.release()
  • examples/claude_agent.py — a real Claude computer-use loop (computer_toolset_20260801) that forwards tool calls straight to dibs. Only makes a live API call if you have Anthropic credentials; otherwise it just proves the wiring.

  • examples/two_agents_demo.py — two agents contend for the desk against a running server, printing the FIFO queueing timeline (acquire, queued, long-poll, granted).

Living with a human

Three modes (dibs mode ask|hands_off|locked, or the dashboard selector):

mode

agents get the desk…

ask (default)

only after you say yes to a consent prompt (being idle is not a yes)

hands_off

any time — you can still take it back

locked

never

In ask mode, if you're active at the keyboard an agent has to ask first: a prompt shows up on screen (bottom-right, doesn't steal focus), on the dashboard as a consent card, with a countdown. Answer with the buttons, or the hotkeys Ctrl+Alt+Shift+Y (allow) / Ctrl+Alt+Shift+N (deny). Saying yes grants a 5-minute consent window so the same agent doesn't have to ask again right away.

Takeover always wins: touch the mouse or keyboard while an agent holds the desk, and everything pauses immediately, the agent loses its dibs, and its next action gets a clear "a human took the desk" error. It resumes on its own after you've been idle for 20 seconds. Ctrl+Alt+Shift+R does the same thing explicitly (take the desk back right now, no agent input required). Ctrl+Alt+Shift+P is the separate manual pause/resume toggle — manual pauses never auto-resume, you have to un-pause them yourself.

What you see on screen

An always-on-top, click-through overlay makes agent activity visible to anyone looking at the monitor:

  • Cyan halo (color configurable) around the cursor with the current agent's name, whenever an agent holds the desk. Hidden otherwise.

  • Top banner: which agent, its purpose, countdown to when its dibs run out. Green the whole time you have the desk (after a takeover or Ctrl+Alt+Shift+R); red only for a manual pause.

  • Quick flash ring on every click, small "typing…" tag while text goes in.

  • The consent prompt described above, bottom-right.

Turn it off with overlay.enabled: false — the hub never depends on it (no display, or Tk failing to start, just logs a warning and carries on).

Safety

  • Pause: dashboard button, Ctrl+Alt+Shift+P, or dibs pause — stops all input actions immediately (423 to callers). Only wait is exempt: nothing looks at or touches the screen while paused.) still work.

  • Failsafe: fling the physical mouse to the top-left screen corner during an action (the pyautogui convention) and it aborts and pauses.

  • allow_launch is off by default — agents can't start arbitrary processes unless you turn it on.

  • Audit log: every action, successful or not, read-only or not, is written to data/dibs.db (SQLite). Screenshots and zooms are saved to data/shots/ (rolling, keep_screenshots deep, default 200) and linked from each audit row.

Exposing to other machines

By default dibs only listens on 127.0.0.1 — nothing outside this machine can reach it. To let other boxes on your Tailscale (e.g. a remote automation runner) drive it, set host: 0.0.0.0 in config.yaml (copy config.example.yaml to start). The moment you do that, loopback's free pass goes away: every route needs a bearer token, including registration and the dashboard.

Config reference

config.yaml in the working directory, or DIBS_CONFIG=<path>. Every key is also settable via env var DIBS_<KEY> (__ for nesting, e.g. DIBS_PRESENCE__IDLE_AFTER_S=10). See config.example.yaml for a starting copy.

key

default

what it does

host

127.0.0.1

bind address; 0.0.0.0 to expose on Tailscale

port

7474

one port for REST, MCP, and the dashboard

data_dir

./data

secrets, agent registry, audit db, screenshots

screen_index

null

force a default monitor; null = primary

max_long_edge

1568

screenshot scaling cap (2576 for larger-image models)

max_pixels

1150000

screenshot scaling cap (3750000 for larger-image models)

lease_default_ttl_s

60

desk lease length if not specified

lease_max_ttl_s

600

hard cap on requested lease length

auto_lease_wait_s

30

how long auto_lease waits to acquire before giving up

allow_local_open_registration

true

loopback callers can POST /v1/agents without a token

dashboard_open_on_loopback

true

dashboard works without a token from 127.0.0.1

allow_launch

false

let agents start processes via the launch action

keep_screenshots

200

rolling cap on files in data/shots/

mode

ask

ask / hands_off / locked — see Living with a human

presence.enabled

true

run the pynput human-presence watcher

presence.idle_after_s

30

how long with no input before you count as "idle"

presence.resume_after_s

20

idle time after a takeover before agents can resume

presence.consent_timeout_s

60

how long an unanswered consent request stays pending

presence.consent_grant_s

300

how long a granted consent window lasts before asking again

presence.deny_cooldown_s

120

how long a denied agent gets an automatic no

presence.consent_grace_s

5.0

after a consent grant (or a promptless grant), how long human input is ignored so accepting the prompt does not read as a takeover. Every monitor dims and a full-width band counts it down: dibs wordmark, which agent got the desk, HANDS OFF N, and a Cancel button. Esc or Cancel hands the desk straight back.

presence.revoke_after_s

2.0

how long continuous mouse movement/scroll can run before it escalates from a pause (lease kept) to a full revoke

overlay.enabled

true

show the cursor halo / banner / consent prompt

overlay.halo_color

#00e5ff

cursor halo color

overlay.banner

true

show the top banner strip

hotkeys.pause

ctrl+alt+shift+p

manual pause/resume toggle

hotkeys.allow

ctrl+alt+shift+y

allow the pending consent request

hotkeys.deny

ctrl+alt+shift+n

deny the pending consent request

hotkeys.release

ctrl+alt+shift+r

take the desk back right now

Layout

path

what's there

dibs/desk.py, keymap.py, actions.py

Windows primitives, key names, action dispatch

dibs/config.py

settings: YAML + env overrides

dibs/registry.py, lease.py, presence.py

agents/tokens, the desk lease, human-presence detection

dibs/audit.py

SQLite audit log + rolling screenshots

dibs/hub.py

the Hub facade — auth, mode/consent, lease gating, runs actions, audit

dibs/server.py

FastAPI app, REST routes, mounts MCP + dashboard

dibs/mcp_server.py

MCP streamable-HTTP server at /mcp

dibs/overlay.py

the on-screen Tk overlay

dibs/__main__.py

CLI: serve, register, agents, status, pause/resume, mode, allow/deny/release, shot, token

dibs/dashboard/

the web dashboard (index.html, app.js, style.css)

clients/python/dibs_client.py

small sync REST client

examples/

claude_agent.py, two_agents_demo.py, overlay_demo.py

scripts/*.ps1

run.ps1, install-task.ps1, uninstall-task.ps1

Dev

uv run pytest                 # everything that doesn't need a real display
uv run pytest -m display      # exercises the real screen/mouse/keyboard/overlay on this box
uv run ruff check .           # lint python code
uv run ruff format .          # format python code
uv run mypy .                 # typecheck python code

npm install                   # install JS dependencies
npm run lint                  # lint dashboard JS
npm run format                # format dashboard files

E2E tests

tests/test_browser_e2e.py (marked display) drives a real, Playwright-launched Chromium window through the dibs hub in-process, against the real desktop -- proving that dibs coordinate math (screenshot-space -> absolute pixels, accounting for OS DPI scaling and dibs' own screenshot downscale) actually lands clicks/scrolls where a real screenshot-driven agent would compute them to go, not just that the internal call chain runs. It never touches the live :7474 server or the user's own windows -- only its own throwaway Chromium instance.

Setup (once): uv run playwright install chromium

Run: uv run pytest -q -m display tests/test_browser_e2e.py

Auto-skips if playwright or its Chromium browser isn't installed, or if not on Windows.

Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

Ensure you run the test suite (uv run pytest) before submitting.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    An MCP server that gives any AI assistant eyes and hands on your desktop — screenshots, clicking, typing, OCR, window management, accessibility-tree queries, workflow recording.
    5
    Apache 2.0
  • A
    license
    Not graded
    quality
    C
    maintenance
    An MCP server that gives AI agents human-like control over Windows via visual perception and simulated mouse and keyboard input, enabling automation of any application without APIs.
    29 npm
    3
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    A Windows computer use agent — FastMCP server that gives AI assistants hands on the real desktop: windows, UI elements, mouse, keyboard, screenshots, OCR, shortcuts, dialogs, and outcome verification.
    36
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    A local, dependency-free MCP server that gives AI agents controlled access to the active Windows desktop, enabling automated interaction with applications through screenshots, clicks, typing, and window management.
    54 npm
    MIT