Skip to main content
Glama
README.md
# settled-computer

**Fast computer use for AI agents.** If your computer-use agent types `time.sleep(2)` between
every click, re-screenshots after every action, and burns vision tokens re-reading an unchanged
screen — that's the problem this fixes.

An MCP server for desktop computer use with **event-driven settling**: every action waits
until the screen actually reacts and stops changing, then returns the settled screenshot and
a one-line verdict. No fixed sleeps, no separate screenshot round trips, no model-side guessing
about *when* to look.

Built for agent loops (Hermes, Claude Desktop, Claude Code, any MCP host). The core idea:
**the server answers "did the UI react, and has it stopped?" at 20 Hz with a ~1 ms frame diff,
so the model doesn't need an extra vision turn just to find out when it is safe to look.**
Whether the reaction was the *right* one is still the model's job: judge by the image.

## Why your computer-use agent is slow (and how this fixes it)

Naive computer-use loops look like this:

```
click -> guess a sleep -> screenshot (full vision turn) -> hope
```

Timings are guesses: too short means acting on half-loaded UIs, too long means wasted seconds.
Each "is it ready yet?" costs a model turn and a screenshot that enters context.

settled-computer replaces that with:

```
click -> server watches the screen -> returns the AFTER frame + verdict
```

- Reacted and settled in 0.4 s → `Screen settled 0.40s after the action.`
- Nothing changed → `No visible change…` (it may simply have had no visible effect: an
  already-focused field, an already-set state). If the model received an image within the last
  45 s and the screen is unchanged, the image is omitted to save vision tokens.
- One region keeps moving (video/spinner) → bails at ~1.5 s with the region's bounding box
  instead of stalling 8 s, and after two consecutive bails the region is auto-ignored.

## Tools

| tool | what it does |
|---|---|
| `act` | **The main one.** Runs 1–8 actions in ONE call: `[{"click":[x,y]}, {"type":"text"}, {"key":"enter"}]`. **Every step is validated before anything runs** (a typo in step 5 cannot leave steps 1–4 half-executed). Each step settles before the next. A timeout always stops the sequence; "no visible change" stops it only for click/dblclick/rightclick/key (`stop_on_no_reaction="auto"`, the default). Use `"never"` when a no-op is expected (e.g. the field is already focused) or `"always"` to stop on any no-op. The stop message lists what ran, what did not, and the exact recovery hint. `screenshot="none"` returns zero images on a clean run. |
| `screenshot` | Look at the screen. **Always returns an image.** Every action already returns the settled frame, so this is for "I have no recent frame". |
| `click` / `type_text` / `press_key` / `scroll` / `drag` / `mouse_move` | Single actions, each returning the settled screen + note. |
| `wait` | Wait for a slow operation to finish changing. |
| `configure` | Tune `quiet_time`, `react_deadline`, `max_wait`, `residual_bail_after`, `ignore_regions`. **Values you set are pinned**: adaptive learning and per-action defaults never override them. `ignore_regions=[]` also clears auto-detected regions. |
| `screen_info` | Image size (coordinates are pixels of THAT image), monitor geometry, platform, auto-ignored regions. |

The server's MCP `instructions` field ships the usage contract to every client, including the
caveat that "no visible change" is not the same as "failed".

## How settling works

1. Grab a **baseline** frame before the action (fast reactions are never missed).
2. Perform the action (on a dedicated input thread, so a long drag or paste never blocks the loop).
3. Poll frames (burst: first poll immediately, 15 ms cadence for 100 ms, then 20 Hz),
   diffing full-resolution in 8×8-pixel cells (uint32 exact compare, ~1 ms per static 1080p frame).
4. Wait for: reaction (any change) → stillness (`quiet_time`) → return.

### Adaptive `max_wait` (and why `quiet_time` is not learned)

`LatencyBook` learns a per-action `max_wait` cap (3× the p95 of past runs, floor 1 s). It records
**timeouts too**, so an action that often times out cannot learn a short cap from its few fast
runs, and it **never overrides a value pinned through `configure`**.

`quiet_time` is deliberately *not* learned. It exists to bridge pauses *between* UI phases
(debounced search, dialog then network fetch), and the time-to-last-motion that a learner can
observe says nothing about those pauses. A learned value would ratchet toward "fast but wrong"
with no feedback signal.

### Config precedence

`configure()`-pinned values › per-action defaults (`type`: quiet 0.12 s / react 0.2 s,
`scroll`: quiet 0.15 s, `hover`: react 0.2 s) › adaptive learning (`max_wait` only).

## Design decisions

- **One lock around every tool.** Hosts may issue parallel tool calls; without serialization
  their settle windows overlap and each verdict is contaminated by the other action.
- **"No image = unchanged" has a time limit** (`SETTLE_MCP_ELIDE_TTL`, default 45 s, `0` disables).
  The server cannot know what is still in the model's context (new chat on a long-lived server,
  hosts that prune old images, compaction), so `screenshot()` never omits an image and action
  results omit one only while the last image the model received is fresh.
- **Auto-ignore with expiry.** After two consecutive residual bails on overlapping regions the
  region is ignored for `SETTLE_MCP_AUTO_IGNORE_SECS` (default 30 s), then re-checked; if it is
  still animating it is re-activated immediately, if not it simply expires.
- **A spinner is not decoration.** The residual note says everything outside the region is stable
  and tells the model to call `wait()` if that region is what it is waiting for.

## Comparison with alternatives

| | settled-computer | naive sleep loop | native per-call drivers (e.g. cua-driver) |
|---|---|---|---|
| waits between actions | measured (frame diff, 20 Hz) | guessed `time.sleep` | none — model re-screenshots to check |
| images per action | 1 (0 when unchanged) | 1–3 | 1 + verification shots |
| model turns for 3 actions | **1** (`act()` batch) | 6+ | 7+ |
| learns your machine | yes (per-action `max_wait`) | no | no |
| video/spinner handling | 1.5 s bail + region hint | stalls forever | stalls or false-settles |

Not a replacement for the action layer (it *uses* pyautogui) — it replaces the guesswork
around it. Works alongside any MCP host: Claude Desktop, Claude Code, Hermes, custom loops.

## Safety — read this first

This server **moves your mouse and types on your real desktop**. It is a prompt-injection
surface: any text, webpage, popup, or document visible on screen can instruct the model
driving it to click and type on your behalf. Treat screen contents as untrusted input.

- **Tool allowlist**: `SETTLE_MCP_TOOLS=screenshot,screen_info,wait` strips all input
  injection (click/type/keys/scroll/drag/act are refused with a clear error). The single
  most effective hardening when you don't need full control.
- Keep the **pyautogui fail-safe on** (default): slam the mouse into the top-left corner to abort.
  The next tool call reports it as a readable error instead of a crash.
- Prefer a VM or a dedicated user session.
- `act()` validates every step before running and halts on timeouts and (by default) on
  click/key no-ops, so a mid-sequence mis-click surfaces instead of compounding.
- macOS needs Screen Recording + Accessibility permission for the host app; Linux needs X11
  (Wayland blocks capture and synthetic input).

## Install

```bash
pip install "settled-computer[desktop]"          # capture + input (what the MCP server needs)
pip install "settled-computer[desktop,fast]"     # + opencv: ~3x faster JPEG encode
pip install "settled-computer[desktop,unicode]"  # + non-ASCII typing (clipboard paste)
```

Extras: `fast` = `opencv-python`, `unicode` = `pyperclip`, `desktop` = `mss`/`pyautogui`/`pillow`.
The core (just `numpy` + `mcp`) is enough to run the engine self-test or browser automation
via Playwright grabbers.

**Linux:** `pip` cannot install `python3-tk`, but pyautogui exits without it:

```bash
sudo apt install python3-tk          # Debian/Ubuntu
```

**Platform status (0.x alpha):** Windows 10/11 is the primary, measured target (numbers
below). Linux/X11 works and is Xvfb-tested in CI; Wayland blocks capture and synthetic
input. **macOS is unverified** — the code path is expected to work with Screen Recording +
Accessibility permissions granted to the host app, but no measured numbers exist yet.
Hence 0.x alpha.

Works with mcp 1.x (`FastMCP`) and 2.x (`MCPServer`).

```bash
settled-computer --check     # verify capture + coordinates (console script)
settled-computer             # serve MCP over stdio
python settled_computer/engine.py --selftest   # 16 synthetic-screen tests, no display needed
```

### Register with an MCP host

Claude Desktop (`claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "settled-computer": {
      "command": "settled-computer",
      "args": []
    }
  }
}
```

Claude Code:

```bash
claude mcp add settled-computer -- settled-computer
```

Hermes (`config.yaml`):

```yaml
mcp_servers:
  settled-computer:
    command: <python-or-venv-path>
    args: ["-m", "settled_computer.server"]
```

Running from a source checkout instead of an install? `python settle_mcp.py` still works
(shim into the package).

### Environment variables

| var | default | meaning |
|---|---|---|
| `SETTLE_MCP_MONITOR` | `1` | monitor index (mss numbering, 1 = primary) |
| `SETTLE_MCP_MAX_WIDTH` | `1280` | max width of returned images (px) |
| `SETTLE_MCP_QUALITY` | `70` | JPEG quality of returned images |
| `SETTLE_MCP_FAILSAFE` | `1` | `0` disables pyautogui's fail-safe |
| `SETTLE_MCP_ELIDE_TTL` | `45` | seconds an unchanged screen may omit its image (`0` = never omit) |
| `SETTLE_MCP_AUTO_IGNORE_SECS` | `30` | how long an auto-detected animating region is ignored before re-checking (`0` = off) |
| `SETTLE_MCP_TOOLS` | *(all)* | allowlist; e.g. `screenshot,screen_info,wait` = observation only, input tools refused |

## Notes the model sees

Every action returns the settled image (unless elided, see above) plus a short note:

- `Screen settled 0.38s after the action.` — proceed.
- `No visible change within 0.30s… It may simply have had no visible effect…` — judge by the image;
  the click may have been fine (already focused / already set) or may have missed.
- `Screen settled 1.51s after the action, except a small region [x0,y0,x1,y1] that keeps animating…`
  + either an auto-ignore confirmation or the exact `configure(ignore_regions=…)` call.
- `Screen was still changing after 8.0s (timeout).` — loading; `wait()` then re-check.
  Includes the motion bounding box when motion is spread across the screen.
- Responses with **no image** say so explicitly and how old the last image is.
- `act` failures: `STOPPED at step N (kind): … Done before it: … Not run: … <recovery hint>`.

## Known limits

- A pixel change is not proof of success: a wrong click that opens the wrong dialog also "settles".
- A blinking caret can register as a reaction, so "no visible change" is less reliable in text fields.
- Pixel-only settling cannot tell "app is thinking" from "app is done" on a static screen.
- Confined-region detection cannot distinguish a video from a progress indicator; the note says so.
- Adaptive `max_wait` is keyed by action type, not by application.

## Performance (Windows 10, 1366×768, as measured by the author)

| operation | settled-computer | native per-call driver |
|---|---|---|
| cheap round trip | **38–55 ms** (persistent stdio) | 340–405 ms (process spawn) |
| screen capture | 16 ms (mss) | 387–963 ms |
| frame diff | 1–5 ms | n/a (model compares screenshots) |
| 3-action sequence | **1 call, 1 image** | ≥7 model turns, ≥4 screenshots |

The last row reflects `act` batching, not settling: any driver could batch. Run your own:
`python bench_mcp.py` (MCP round trips) and `python bench_native.py` (cua-driver).
`python find_motion.py` locates what keeps changing on your screen (e.g. to pick `ignore_regions`).

## Safety

This server lets a model move your mouse and type on your real desktop.

- Keep the **pyautogui fail-safe on** (default): slam the mouse into the top-left corner to abort.
  The next tool call reports it as a readable error instead of a crash.
- Prefer a VM or a dedicated user session.
- `act()` validates every step before running and halts on timeouts and (by default) on
  click/key no-ops, so a mid-sequence mis-click surfaces instead of compounding.
- macOS needs Screen Recording + Accessibility permission for the host app; Linux needs X11
  (Wayland blocks capture and synthetic input).

## Files

```
settled_computer/
├── engine.py        Settle engine: wait_settled, act_and_settle, LatencyBook, selftest (no MCP dep)
└── server.py        MCP server: 11 tools, encoding, act() sequencer, tool allowlist
settle.py            shim -> settled_computer.engine (old checkouts)
settle_mcp.py        shim -> settled_computer.server (old MCP registrations)
bench_mcp.py         MCP round-trip benchmark (persistent stdio client)
bench_native.py      cua-driver benchmark (subprocess per call, for comparison)
find_motion.py       Locate perpetually-animating screen regions (ignore_regions picker)
```

## License

MIT (see `LICENSE`). Chosen so MCP hosts and agent distributions can bundle it freely;
any future paid tier will be an open-core split (hosted/managed features around the same
open server), not a relicense.