settled-computer 0.1.0a1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- settled_computer-0.1.0a1/LICENSE +21 -0
- settled_computer-0.1.0a1/PKG-INFO +284 -0
- settled_computer-0.1.0a1/README.md +247 -0
- settled_computer-0.1.0a1/pyproject.toml +55 -0
- settled_computer-0.1.0a1/settled_computer/__init__.py +39 -0
- settled_computer-0.1.0a1/settled_computer/engine.py +581 -0
- settled_computer-0.1.0a1/settled_computer/server.py +754 -0
- settled_computer-0.1.0a1/settled_computer.egg-info/PKG-INFO +284 -0
- settled_computer-0.1.0a1/settled_computer.egg-info/SOURCES.txt +12 -0
- settled_computer-0.1.0a1/settled_computer.egg-info/dependency_links.txt +1 -0
- settled_computer-0.1.0a1/settled_computer.egg-info/entry_points.txt +2 -0
- settled_computer-0.1.0a1/settled_computer.egg-info/requires.txt +16 -0
- settled_computer-0.1.0a1/settled_computer.egg-info/top_level.txt +1 -0
- settled_computer-0.1.0a1/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Chiheb (settled-computer)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, STRICT LIABILITY OR TORT
|
|
20
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING FROM, OUT OF OR IN CONNECTION
|
|
21
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: settled-computer
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: MCP server for desktop computer use with event-driven settling: every action waits until the screen reacts and stops changing, then returns the settled screenshot.
|
|
5
|
+
Author: Chiheb
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/chihebnabi/settled-computer
|
|
8
|
+
Project-URL: Issues, https://github.com/chihebnabi/settled-computer/issues
|
|
9
|
+
Keywords: mcp,computer-use,agent,automation,screen-capture,settle
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: numpy<3,>=1.24
|
|
25
|
+
Requires-Dist: mcp<3,>=1.2
|
|
26
|
+
Provides-Extra: desktop
|
|
27
|
+
Requires-Dist: mss<11,>=9; extra == "desktop"
|
|
28
|
+
Requires-Dist: pyautogui<1,>=0.9.14; extra == "desktop"
|
|
29
|
+
Requires-Dist: pillow<12,>=10; extra == "desktop"
|
|
30
|
+
Provides-Extra: fast
|
|
31
|
+
Requires-Dist: opencv-python<5,>=4.8; extra == "fast"
|
|
32
|
+
Provides-Extra: unicode
|
|
33
|
+
Requires-Dist: pyperclip<2,>=1.8; extra == "unicode"
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# settled-computer
|
|
39
|
+
|
|
40
|
+
An MCP server for desktop computer use with **event-driven settling**: every action waits
|
|
41
|
+
until the screen actually reacts and stops changing, then returns the settled screenshot and
|
|
42
|
+
a one-line verdict. No fixed sleeps, no separate screenshot round trips, no model-side guessing
|
|
43
|
+
about *when* to look.
|
|
44
|
+
|
|
45
|
+
Built for agent loops (Hermes, Claude Desktop, Claude Code, any MCP host). The core idea:
|
|
46
|
+
**the server answers "did the UI react, and has it stopped?" at 20 Hz with a ~1 ms frame diff,
|
|
47
|
+
so the model doesn't need an extra vision turn just to find out when it is safe to look.**
|
|
48
|
+
Whether the reaction was the *right* one is still the model's job: judge by the image.
|
|
49
|
+
|
|
50
|
+
## Why
|
|
51
|
+
|
|
52
|
+
Naive computer-use loops look like this:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
click -> guess a sleep -> screenshot (full vision turn) -> hope
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Timings are guesses: too short means acting on half-loaded UIs, too long means wasted seconds.
|
|
59
|
+
Each "is it ready yet?" costs a model turn and a screenshot that enters context.
|
|
60
|
+
|
|
61
|
+
settled-computer replaces that with:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
click -> server watches the screen -> returns the AFTER frame + verdict
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- Reacted and settled in 0.4 s → `Screen settled 0.40s after the action.`
|
|
68
|
+
- Nothing changed → `No visible change…` (it may simply have had no visible effect: an
|
|
69
|
+
already-focused field, an already-set state). If the model received an image within the last
|
|
70
|
+
45 s and the screen is unchanged, the image is omitted to save vision tokens.
|
|
71
|
+
- One region keeps moving (video/spinner) → bails at ~1.5 s with the region's bounding box
|
|
72
|
+
instead of stalling 8 s, and after two consecutive bails the region is auto-ignored.
|
|
73
|
+
|
|
74
|
+
## Tools
|
|
75
|
+
|
|
76
|
+
| tool | what it does |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `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. |
|
|
79
|
+
| `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". |
|
|
80
|
+
| `click` / `type_text` / `press_key` / `scroll` / `drag` / `mouse_move` | Single actions, each returning the settled screen + note. |
|
|
81
|
+
| `wait` | Wait for a slow operation to finish changing. |
|
|
82
|
+
| `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. |
|
|
83
|
+
| `screen_info` | Image size (coordinates are pixels of THAT image), monitor geometry, platform, auto-ignored regions. |
|
|
84
|
+
|
|
85
|
+
The server's MCP `instructions` field ships the usage contract to every client, including the
|
|
86
|
+
caveat that "no visible change" is not the same as "failed".
|
|
87
|
+
|
|
88
|
+
## How settling works
|
|
89
|
+
|
|
90
|
+
1. Grab a **baseline** frame before the action (fast reactions are never missed).
|
|
91
|
+
2. Perform the action (on a dedicated input thread, so a long drag or paste never blocks the loop).
|
|
92
|
+
3. Poll frames (burst: first poll immediately, 15 ms cadence for 100 ms, then 20 Hz),
|
|
93
|
+
diffing full-resolution in 8×8-pixel cells (uint32 exact compare, ~1 ms per static 1080p frame).
|
|
94
|
+
4. Wait for: reaction (any change) → stillness (`quiet_time`) → return.
|
|
95
|
+
|
|
96
|
+
### Adaptive `max_wait` (and why `quiet_time` is not learned)
|
|
97
|
+
|
|
98
|
+
`LatencyBook` learns a per-action `max_wait` cap (3× the p95 of past runs, floor 1 s). It records
|
|
99
|
+
**timeouts too**, so an action that often times out cannot learn a short cap from its few fast
|
|
100
|
+
runs, and it **never overrides a value pinned through `configure`**.
|
|
101
|
+
|
|
102
|
+
`quiet_time` is deliberately *not* learned. It exists to bridge pauses *between* UI phases
|
|
103
|
+
(debounced search, dialog then network fetch), and the time-to-last-motion that a learner can
|
|
104
|
+
observe says nothing about those pauses. A learned value would ratchet toward "fast but wrong"
|
|
105
|
+
with no feedback signal.
|
|
106
|
+
|
|
107
|
+
### Config precedence
|
|
108
|
+
|
|
109
|
+
`configure()`-pinned values › per-action defaults (`type`: quiet 0.12 s / react 0.2 s,
|
|
110
|
+
`scroll`: quiet 0.15 s, `hover`: react 0.2 s) › adaptive learning (`max_wait` only).
|
|
111
|
+
|
|
112
|
+
## Design decisions
|
|
113
|
+
|
|
114
|
+
- **One lock around every tool.** Hosts may issue parallel tool calls; without serialization
|
|
115
|
+
their settle windows overlap and each verdict is contaminated by the other action.
|
|
116
|
+
- **"No image = unchanged" has a time limit** (`SETTLE_MCP_ELIDE_TTL`, default 45 s, `0` disables).
|
|
117
|
+
The server cannot know what is still in the model's context (new chat on a long-lived server,
|
|
118
|
+
hosts that prune old images, compaction), so `screenshot()` never omits an image and action
|
|
119
|
+
results omit one only while the last image the model received is fresh.
|
|
120
|
+
- **Auto-ignore with expiry.** After two consecutive residual bails on overlapping regions the
|
|
121
|
+
region is ignored for `SETTLE_MCP_AUTO_IGNORE_SECS` (default 30 s), then re-checked; if it is
|
|
122
|
+
still animating it is re-activated immediately, if not it simply expires.
|
|
123
|
+
- **A spinner is not decoration.** The residual note says everything outside the region is stable
|
|
124
|
+
and tells the model to call `wait()` if that region is what it is waiting for.
|
|
125
|
+
|
|
126
|
+
## Safety — read this first
|
|
127
|
+
|
|
128
|
+
This server **moves your mouse and types on your real desktop**. It is a prompt-injection
|
|
129
|
+
surface: any text, webpage, popup, or document visible on screen can instruct the model
|
|
130
|
+
driving it to click and type on your behalf. Treat screen contents as untrusted input.
|
|
131
|
+
|
|
132
|
+
- **Tool allowlist**: `SETTLE_MCP_TOOLS=screenshot,screen_info,wait` strips all input
|
|
133
|
+
injection (click/type/keys/scroll/drag/act are refused with a clear error). The single
|
|
134
|
+
most effective hardening when you don't need full control.
|
|
135
|
+
- Keep the **pyautogui fail-safe on** (default): slam the mouse into the top-left corner to abort.
|
|
136
|
+
The next tool call reports it as a readable error instead of a crash.
|
|
137
|
+
- Prefer a VM or a dedicated user session.
|
|
138
|
+
- `act()` validates every step before running and halts on timeouts and (by default) on
|
|
139
|
+
click/key no-ops, so a mid-sequence mis-click surfaces instead of compounding.
|
|
140
|
+
- macOS needs Screen Recording + Accessibility permission for the host app; Linux needs X11
|
|
141
|
+
(Wayland blocks capture and synthetic input).
|
|
142
|
+
|
|
143
|
+
## Install
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
pip install "settled-computer[desktop]" # capture + input (what the MCP server needs)
|
|
147
|
+
pip install "settled-computer[desktop,fast]" # + opencv: ~3x faster JPEG encode
|
|
148
|
+
pip install "settled-computer[desktop,unicode]" # + non-ASCII typing (clipboard paste)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Extras: `fast` = `opencv-python`, `unicode` = `pyperclip`, `desktop` = `mss`/`pyautogui`/`pillow`.
|
|
152
|
+
The core (just `numpy` + `mcp`) is enough to run the engine self-test or browser automation
|
|
153
|
+
via Playwright grabbers.
|
|
154
|
+
|
|
155
|
+
**Linux:** `pip` cannot install `python3-tk`, but pyautogui exits without it:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
sudo apt install python3-tk # Debian/Ubuntu
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Platform status (0.x alpha):** Windows 10/11 is the primary, measured target (numbers
|
|
162
|
+
below). Linux/X11 works and is Xvfb-tested in CI; Wayland blocks capture and synthetic
|
|
163
|
+
input. **macOS is unverified** — the code path is expected to work with Screen Recording +
|
|
164
|
+
Accessibility permissions granted to the host app, but no measured numbers exist yet.
|
|
165
|
+
Hence 0.x alpha.
|
|
166
|
+
|
|
167
|
+
Works with mcp 1.x (`FastMCP`) and 2.x (`MCPServer`).
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
settled-computer --check # verify capture + coordinates (console script)
|
|
171
|
+
settled-computer # serve MCP over stdio
|
|
172
|
+
python settled_computer/engine.py --selftest # 16 synthetic-screen tests, no display needed
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Register with an MCP host
|
|
176
|
+
|
|
177
|
+
Claude Desktop (`claude_desktop_config.json`):
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"mcpServers": {
|
|
182
|
+
"settled-computer": {
|
|
183
|
+
"command": "settled-computer",
|
|
184
|
+
"args": []
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Claude Code:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
claude mcp add settled-computer -- settled-computer
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Hermes (`config.yaml`):
|
|
197
|
+
|
|
198
|
+
```yaml
|
|
199
|
+
mcp_servers:
|
|
200
|
+
settled-computer:
|
|
201
|
+
command: <python-or-venv-path>
|
|
202
|
+
args: ["-m", "settled_computer.server"]
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Running from a source checkout instead of an install? `python settle_mcp.py` still works
|
|
206
|
+
(shim into the package).
|
|
207
|
+
|
|
208
|
+
### Environment variables
|
|
209
|
+
|
|
210
|
+
| var | default | meaning |
|
|
211
|
+
|---|---|---|
|
|
212
|
+
| `SETTLE_MCP_MONITOR` | `1` | monitor index (mss numbering, 1 = primary) |
|
|
213
|
+
| `SETTLE_MCP_MAX_WIDTH` | `1280` | max width of returned images (px) |
|
|
214
|
+
| `SETTLE_MCP_QUALITY` | `70` | JPEG quality of returned images |
|
|
215
|
+
| `SETTLE_MCP_FAILSAFE` | `1` | `0` disables pyautogui's fail-safe |
|
|
216
|
+
| `SETTLE_MCP_ELIDE_TTL` | `45` | seconds an unchanged screen may omit its image (`0` = never omit) |
|
|
217
|
+
| `SETTLE_MCP_AUTO_IGNORE_SECS` | `30` | how long an auto-detected animating region is ignored before re-checking (`0` = off) |
|
|
218
|
+
| `SETTLE_MCP_TOOLS` | *(all)* | allowlist; e.g. `screenshot,screen_info,wait` = observation only, input tools refused |
|
|
219
|
+
|
|
220
|
+
## Notes the model sees
|
|
221
|
+
|
|
222
|
+
Every action returns the settled image (unless elided, see above) plus a short note:
|
|
223
|
+
|
|
224
|
+
- `Screen settled 0.38s after the action.` — proceed.
|
|
225
|
+
- `No visible change within 0.30s… It may simply have had no visible effect…` — judge by the image;
|
|
226
|
+
the click may have been fine (already focused / already set) or may have missed.
|
|
227
|
+
- `Screen settled 1.51s after the action, except a small region [x0,y0,x1,y1] that keeps animating…`
|
|
228
|
+
+ either an auto-ignore confirmation or the exact `configure(ignore_regions=…)` call.
|
|
229
|
+
- `Screen was still changing after 8.0s (timeout).` — loading; `wait()` then re-check.
|
|
230
|
+
Includes the motion bounding box when motion is spread across the screen.
|
|
231
|
+
- Responses with **no image** say so explicitly and how old the last image is.
|
|
232
|
+
- `act` failures: `STOPPED at step N (kind): … Done before it: … Not run: … <recovery hint>`.
|
|
233
|
+
|
|
234
|
+
## Known limits
|
|
235
|
+
|
|
236
|
+
- A pixel change is not proof of success: a wrong click that opens the wrong dialog also "settles".
|
|
237
|
+
- A blinking caret can register as a reaction, so "no visible change" is less reliable in text fields.
|
|
238
|
+
- Pixel-only settling cannot tell "app is thinking" from "app is done" on a static screen.
|
|
239
|
+
- Confined-region detection cannot distinguish a video from a progress indicator; the note says so.
|
|
240
|
+
- Adaptive `max_wait` is keyed by action type, not by application.
|
|
241
|
+
|
|
242
|
+
## Performance (Windows 10, 1366×768, as measured by the author)
|
|
243
|
+
|
|
244
|
+
| operation | settled-computer | native per-call driver |
|
|
245
|
+
|---|---|---|
|
|
246
|
+
| cheap round trip | **38–55 ms** (persistent stdio) | 340–405 ms (process spawn) |
|
|
247
|
+
| screen capture | 16 ms (mss) | 387–963 ms |
|
|
248
|
+
| frame diff | 1–5 ms | n/a (model compares screenshots) |
|
|
249
|
+
| 3-action sequence | **1 call, 1 image** | ≥7 model turns, ≥4 screenshots |
|
|
250
|
+
|
|
251
|
+
The last row reflects `act` batching, not settling: any driver could batch. Run your own:
|
|
252
|
+
`python bench_mcp.py` (MCP round trips) and `python bench_native.py` (cua-driver).
|
|
253
|
+
`python find_motion.py` locates what keeps changing on your screen (e.g. to pick `ignore_regions`).
|
|
254
|
+
|
|
255
|
+
## Safety
|
|
256
|
+
|
|
257
|
+
This server lets a model move your mouse and type on your real desktop.
|
|
258
|
+
|
|
259
|
+
- Keep the **pyautogui fail-safe on** (default): slam the mouse into the top-left corner to abort.
|
|
260
|
+
The next tool call reports it as a readable error instead of a crash.
|
|
261
|
+
- Prefer a VM or a dedicated user session.
|
|
262
|
+
- `act()` validates every step before running and halts on timeouts and (by default) on
|
|
263
|
+
click/key no-ops, so a mid-sequence mis-click surfaces instead of compounding.
|
|
264
|
+
- macOS needs Screen Recording + Accessibility permission for the host app; Linux needs X11
|
|
265
|
+
(Wayland blocks capture and synthetic input).
|
|
266
|
+
|
|
267
|
+
## Files
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
settled_computer/
|
|
271
|
+
├── engine.py Settle engine: wait_settled, act_and_settle, LatencyBook, selftest (no MCP dep)
|
|
272
|
+
└── server.py MCP server: 11 tools, encoding, act() sequencer, tool allowlist
|
|
273
|
+
settle.py shim -> settled_computer.engine (old checkouts)
|
|
274
|
+
settle_mcp.py shim -> settled_computer.server (old MCP registrations)
|
|
275
|
+
bench_mcp.py MCP round-trip benchmark (persistent stdio client)
|
|
276
|
+
bench_native.py cua-driver benchmark (subprocess per call, for comparison)
|
|
277
|
+
find_motion.py Locate perpetually-animating screen regions (ignore_regions picker)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## License
|
|
281
|
+
|
|
282
|
+
MIT (see `LICENSE`). Chosen so MCP hosts and agent distributions can bundle it freely;
|
|
283
|
+
any future paid tier will be an open-core split (hosted/managed features around the same
|
|
284
|
+
open server), not a relicense.
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# settled-computer
|
|
2
|
+
|
|
3
|
+
An MCP server for desktop computer use with **event-driven settling**: every action waits
|
|
4
|
+
until the screen actually reacts and stops changing, then returns the settled screenshot and
|
|
5
|
+
a one-line verdict. No fixed sleeps, no separate screenshot round trips, no model-side guessing
|
|
6
|
+
about *when* to look.
|
|
7
|
+
|
|
8
|
+
Built for agent loops (Hermes, Claude Desktop, Claude Code, any MCP host). The core idea:
|
|
9
|
+
**the server answers "did the UI react, and has it stopped?" at 20 Hz with a ~1 ms frame diff,
|
|
10
|
+
so the model doesn't need an extra vision turn just to find out when it is safe to look.**
|
|
11
|
+
Whether the reaction was the *right* one is still the model's job: judge by the image.
|
|
12
|
+
|
|
13
|
+
## Why
|
|
14
|
+
|
|
15
|
+
Naive computer-use loops look like this:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
click -> guess a sleep -> screenshot (full vision turn) -> hope
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Timings are guesses: too short means acting on half-loaded UIs, too long means wasted seconds.
|
|
22
|
+
Each "is it ready yet?" costs a model turn and a screenshot that enters context.
|
|
23
|
+
|
|
24
|
+
settled-computer replaces that with:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
click -> server watches the screen -> returns the AFTER frame + verdict
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- Reacted and settled in 0.4 s → `Screen settled 0.40s after the action.`
|
|
31
|
+
- Nothing changed → `No visible change…` (it may simply have had no visible effect: an
|
|
32
|
+
already-focused field, an already-set state). If the model received an image within the last
|
|
33
|
+
45 s and the screen is unchanged, the image is omitted to save vision tokens.
|
|
34
|
+
- One region keeps moving (video/spinner) → bails at ~1.5 s with the region's bounding box
|
|
35
|
+
instead of stalling 8 s, and after two consecutive bails the region is auto-ignored.
|
|
36
|
+
|
|
37
|
+
## Tools
|
|
38
|
+
|
|
39
|
+
| tool | what it does |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `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. |
|
|
42
|
+
| `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". |
|
|
43
|
+
| `click` / `type_text` / `press_key` / `scroll` / `drag` / `mouse_move` | Single actions, each returning the settled screen + note. |
|
|
44
|
+
| `wait` | Wait for a slow operation to finish changing. |
|
|
45
|
+
| `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. |
|
|
46
|
+
| `screen_info` | Image size (coordinates are pixels of THAT image), monitor geometry, platform, auto-ignored regions. |
|
|
47
|
+
|
|
48
|
+
The server's MCP `instructions` field ships the usage contract to every client, including the
|
|
49
|
+
caveat that "no visible change" is not the same as "failed".
|
|
50
|
+
|
|
51
|
+
## How settling works
|
|
52
|
+
|
|
53
|
+
1. Grab a **baseline** frame before the action (fast reactions are never missed).
|
|
54
|
+
2. Perform the action (on a dedicated input thread, so a long drag or paste never blocks the loop).
|
|
55
|
+
3. Poll frames (burst: first poll immediately, 15 ms cadence for 100 ms, then 20 Hz),
|
|
56
|
+
diffing full-resolution in 8×8-pixel cells (uint32 exact compare, ~1 ms per static 1080p frame).
|
|
57
|
+
4. Wait for: reaction (any change) → stillness (`quiet_time`) → return.
|
|
58
|
+
|
|
59
|
+
### Adaptive `max_wait` (and why `quiet_time` is not learned)
|
|
60
|
+
|
|
61
|
+
`LatencyBook` learns a per-action `max_wait` cap (3× the p95 of past runs, floor 1 s). It records
|
|
62
|
+
**timeouts too**, so an action that often times out cannot learn a short cap from its few fast
|
|
63
|
+
runs, and it **never overrides a value pinned through `configure`**.
|
|
64
|
+
|
|
65
|
+
`quiet_time` is deliberately *not* learned. It exists to bridge pauses *between* UI phases
|
|
66
|
+
(debounced search, dialog then network fetch), and the time-to-last-motion that a learner can
|
|
67
|
+
observe says nothing about those pauses. A learned value would ratchet toward "fast but wrong"
|
|
68
|
+
with no feedback signal.
|
|
69
|
+
|
|
70
|
+
### Config precedence
|
|
71
|
+
|
|
72
|
+
`configure()`-pinned values › per-action defaults (`type`: quiet 0.12 s / react 0.2 s,
|
|
73
|
+
`scroll`: quiet 0.15 s, `hover`: react 0.2 s) › adaptive learning (`max_wait` only).
|
|
74
|
+
|
|
75
|
+
## Design decisions
|
|
76
|
+
|
|
77
|
+
- **One lock around every tool.** Hosts may issue parallel tool calls; without serialization
|
|
78
|
+
their settle windows overlap and each verdict is contaminated by the other action.
|
|
79
|
+
- **"No image = unchanged" has a time limit** (`SETTLE_MCP_ELIDE_TTL`, default 45 s, `0` disables).
|
|
80
|
+
The server cannot know what is still in the model's context (new chat on a long-lived server,
|
|
81
|
+
hosts that prune old images, compaction), so `screenshot()` never omits an image and action
|
|
82
|
+
results omit one only while the last image the model received is fresh.
|
|
83
|
+
- **Auto-ignore with expiry.** After two consecutive residual bails on overlapping regions the
|
|
84
|
+
region is ignored for `SETTLE_MCP_AUTO_IGNORE_SECS` (default 30 s), then re-checked; if it is
|
|
85
|
+
still animating it is re-activated immediately, if not it simply expires.
|
|
86
|
+
- **A spinner is not decoration.** The residual note says everything outside the region is stable
|
|
87
|
+
and tells the model to call `wait()` if that region is what it is waiting for.
|
|
88
|
+
|
|
89
|
+
## Safety — read this first
|
|
90
|
+
|
|
91
|
+
This server **moves your mouse and types on your real desktop**. It is a prompt-injection
|
|
92
|
+
surface: any text, webpage, popup, or document visible on screen can instruct the model
|
|
93
|
+
driving it to click and type on your behalf. Treat screen contents as untrusted input.
|
|
94
|
+
|
|
95
|
+
- **Tool allowlist**: `SETTLE_MCP_TOOLS=screenshot,screen_info,wait` strips all input
|
|
96
|
+
injection (click/type/keys/scroll/drag/act are refused with a clear error). The single
|
|
97
|
+
most effective hardening when you don't need full control.
|
|
98
|
+
- Keep the **pyautogui fail-safe on** (default): slam the mouse into the top-left corner to abort.
|
|
99
|
+
The next tool call reports it as a readable error instead of a crash.
|
|
100
|
+
- Prefer a VM or a dedicated user session.
|
|
101
|
+
- `act()` validates every step before running and halts on timeouts and (by default) on
|
|
102
|
+
click/key no-ops, so a mid-sequence mis-click surfaces instead of compounding.
|
|
103
|
+
- macOS needs Screen Recording + Accessibility permission for the host app; Linux needs X11
|
|
104
|
+
(Wayland blocks capture and synthetic input).
|
|
105
|
+
|
|
106
|
+
## Install
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
pip install "settled-computer[desktop]" # capture + input (what the MCP server needs)
|
|
110
|
+
pip install "settled-computer[desktop,fast]" # + opencv: ~3x faster JPEG encode
|
|
111
|
+
pip install "settled-computer[desktop,unicode]" # + non-ASCII typing (clipboard paste)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Extras: `fast` = `opencv-python`, `unicode` = `pyperclip`, `desktop` = `mss`/`pyautogui`/`pillow`.
|
|
115
|
+
The core (just `numpy` + `mcp`) is enough to run the engine self-test or browser automation
|
|
116
|
+
via Playwright grabbers.
|
|
117
|
+
|
|
118
|
+
**Linux:** `pip` cannot install `python3-tk`, but pyautogui exits without it:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
sudo apt install python3-tk # Debian/Ubuntu
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Platform status (0.x alpha):** Windows 10/11 is the primary, measured target (numbers
|
|
125
|
+
below). Linux/X11 works and is Xvfb-tested in CI; Wayland blocks capture and synthetic
|
|
126
|
+
input. **macOS is unverified** — the code path is expected to work with Screen Recording +
|
|
127
|
+
Accessibility permissions granted to the host app, but no measured numbers exist yet.
|
|
128
|
+
Hence 0.x alpha.
|
|
129
|
+
|
|
130
|
+
Works with mcp 1.x (`FastMCP`) and 2.x (`MCPServer`).
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
settled-computer --check # verify capture + coordinates (console script)
|
|
134
|
+
settled-computer # serve MCP over stdio
|
|
135
|
+
python settled_computer/engine.py --selftest # 16 synthetic-screen tests, no display needed
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Register with an MCP host
|
|
139
|
+
|
|
140
|
+
Claude Desktop (`claude_desktop_config.json`):
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"mcpServers": {
|
|
145
|
+
"settled-computer": {
|
|
146
|
+
"command": "settled-computer",
|
|
147
|
+
"args": []
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Claude Code:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
claude mcp add settled-computer -- settled-computer
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Hermes (`config.yaml`):
|
|
160
|
+
|
|
161
|
+
```yaml
|
|
162
|
+
mcp_servers:
|
|
163
|
+
settled-computer:
|
|
164
|
+
command: <python-or-venv-path>
|
|
165
|
+
args: ["-m", "settled_computer.server"]
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Running from a source checkout instead of an install? `python settle_mcp.py` still works
|
|
169
|
+
(shim into the package).
|
|
170
|
+
|
|
171
|
+
### Environment variables
|
|
172
|
+
|
|
173
|
+
| var | default | meaning |
|
|
174
|
+
|---|---|---|
|
|
175
|
+
| `SETTLE_MCP_MONITOR` | `1` | monitor index (mss numbering, 1 = primary) |
|
|
176
|
+
| `SETTLE_MCP_MAX_WIDTH` | `1280` | max width of returned images (px) |
|
|
177
|
+
| `SETTLE_MCP_QUALITY` | `70` | JPEG quality of returned images |
|
|
178
|
+
| `SETTLE_MCP_FAILSAFE` | `1` | `0` disables pyautogui's fail-safe |
|
|
179
|
+
| `SETTLE_MCP_ELIDE_TTL` | `45` | seconds an unchanged screen may omit its image (`0` = never omit) |
|
|
180
|
+
| `SETTLE_MCP_AUTO_IGNORE_SECS` | `30` | how long an auto-detected animating region is ignored before re-checking (`0` = off) |
|
|
181
|
+
| `SETTLE_MCP_TOOLS` | *(all)* | allowlist; e.g. `screenshot,screen_info,wait` = observation only, input tools refused |
|
|
182
|
+
|
|
183
|
+
## Notes the model sees
|
|
184
|
+
|
|
185
|
+
Every action returns the settled image (unless elided, see above) plus a short note:
|
|
186
|
+
|
|
187
|
+
- `Screen settled 0.38s after the action.` — proceed.
|
|
188
|
+
- `No visible change within 0.30s… It may simply have had no visible effect…` — judge by the image;
|
|
189
|
+
the click may have been fine (already focused / already set) or may have missed.
|
|
190
|
+
- `Screen settled 1.51s after the action, except a small region [x0,y0,x1,y1] that keeps animating…`
|
|
191
|
+
+ either an auto-ignore confirmation or the exact `configure(ignore_regions=…)` call.
|
|
192
|
+
- `Screen was still changing after 8.0s (timeout).` — loading; `wait()` then re-check.
|
|
193
|
+
Includes the motion bounding box when motion is spread across the screen.
|
|
194
|
+
- Responses with **no image** say so explicitly and how old the last image is.
|
|
195
|
+
- `act` failures: `STOPPED at step N (kind): … Done before it: … Not run: … <recovery hint>`.
|
|
196
|
+
|
|
197
|
+
## Known limits
|
|
198
|
+
|
|
199
|
+
- A pixel change is not proof of success: a wrong click that opens the wrong dialog also "settles".
|
|
200
|
+
- A blinking caret can register as a reaction, so "no visible change" is less reliable in text fields.
|
|
201
|
+
- Pixel-only settling cannot tell "app is thinking" from "app is done" on a static screen.
|
|
202
|
+
- Confined-region detection cannot distinguish a video from a progress indicator; the note says so.
|
|
203
|
+
- Adaptive `max_wait` is keyed by action type, not by application.
|
|
204
|
+
|
|
205
|
+
## Performance (Windows 10, 1366×768, as measured by the author)
|
|
206
|
+
|
|
207
|
+
| operation | settled-computer | native per-call driver |
|
|
208
|
+
|---|---|---|
|
|
209
|
+
| cheap round trip | **38–55 ms** (persistent stdio) | 340–405 ms (process spawn) |
|
|
210
|
+
| screen capture | 16 ms (mss) | 387–963 ms |
|
|
211
|
+
| frame diff | 1–5 ms | n/a (model compares screenshots) |
|
|
212
|
+
| 3-action sequence | **1 call, 1 image** | ≥7 model turns, ≥4 screenshots |
|
|
213
|
+
|
|
214
|
+
The last row reflects `act` batching, not settling: any driver could batch. Run your own:
|
|
215
|
+
`python bench_mcp.py` (MCP round trips) and `python bench_native.py` (cua-driver).
|
|
216
|
+
`python find_motion.py` locates what keeps changing on your screen (e.g. to pick `ignore_regions`).
|
|
217
|
+
|
|
218
|
+
## Safety
|
|
219
|
+
|
|
220
|
+
This server lets a model move your mouse and type on your real desktop.
|
|
221
|
+
|
|
222
|
+
- Keep the **pyautogui fail-safe on** (default): slam the mouse into the top-left corner to abort.
|
|
223
|
+
The next tool call reports it as a readable error instead of a crash.
|
|
224
|
+
- Prefer a VM or a dedicated user session.
|
|
225
|
+
- `act()` validates every step before running and halts on timeouts and (by default) on
|
|
226
|
+
click/key no-ops, so a mid-sequence mis-click surfaces instead of compounding.
|
|
227
|
+
- macOS needs Screen Recording + Accessibility permission for the host app; Linux needs X11
|
|
228
|
+
(Wayland blocks capture and synthetic input).
|
|
229
|
+
|
|
230
|
+
## Files
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
settled_computer/
|
|
234
|
+
├── engine.py Settle engine: wait_settled, act_and_settle, LatencyBook, selftest (no MCP dep)
|
|
235
|
+
└── server.py MCP server: 11 tools, encoding, act() sequencer, tool allowlist
|
|
236
|
+
settle.py shim -> settled_computer.engine (old checkouts)
|
|
237
|
+
settle_mcp.py shim -> settled_computer.server (old MCP registrations)
|
|
238
|
+
bench_mcp.py MCP round-trip benchmark (persistent stdio client)
|
|
239
|
+
bench_native.py cua-driver benchmark (subprocess per call, for comparison)
|
|
240
|
+
find_motion.py Locate perpetually-animating screen regions (ignore_regions picker)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## License
|
|
244
|
+
|
|
245
|
+
MIT (see `LICENSE`). Chosen so MCP hosts and agent distributions can bundle it freely;
|
|
246
|
+
any future paid tier will be an open-core split (hosted/managed features around the same
|
|
247
|
+
open server), not a relicense.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "settled-computer"
|
|
7
|
+
version = "0.1.0a1"
|
|
8
|
+
description = "MCP server for desktop computer use with event-driven settling: every action waits until the screen reacts and stops changing, then returns the settled screenshot."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Chiheb" }]
|
|
13
|
+
keywords = ["mcp", "computer-use", "agent", "automation", "screen-capture", "settle"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: Microsoft :: Windows",
|
|
19
|
+
"Operating System :: POSIX :: Linux",
|
|
20
|
+
"Operating System :: MacOS",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
|
+
]
|
|
27
|
+
# mss is desktop-capture; on macOS/Linux it may be absent but the engine's
|
|
28
|
+
# selftest and browser grabbers still work without a display. Keep core lean.
|
|
29
|
+
dependencies = [
|
|
30
|
+
"numpy>=1.24,<3",
|
|
31
|
+
"mcp>=1.2,<3",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
# desktop actions + capture (what the MCP server needs on a real desktop)
|
|
36
|
+
desktop = [
|
|
37
|
+
"mss>=9,<11",
|
|
38
|
+
"pyautogui>=0.9.14,<1",
|
|
39
|
+
"pillow>=10,<12",
|
|
40
|
+
]
|
|
41
|
+
# ~3x faster JPEG encode than the PIL fallback
|
|
42
|
+
fast = ["opencv-python>=4.8,<5"]
|
|
43
|
+
# non-ASCII typing (pastes via clipboard)
|
|
44
|
+
unicode = ["pyperclip>=1.8,<2"]
|
|
45
|
+
dev = ["pytest>=8"]
|
|
46
|
+
|
|
47
|
+
[project.urls]
|
|
48
|
+
Homepage = "https://github.com/chihebnabi/settled-computer"
|
|
49
|
+
Issues = "https://github.com/chihebnabi/settled-computer/issues"
|
|
50
|
+
|
|
51
|
+
[project.scripts]
|
|
52
|
+
settled-computer = "settled_computer.server:main"
|
|
53
|
+
|
|
54
|
+
[tool.setuptools]
|
|
55
|
+
packages = ["settled_computer"]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""settled-computer: event-driven settling for computer-use agents.
|
|
2
|
+
|
|
3
|
+
Every action waits until the screen reacts and stops changing, then returns the
|
|
4
|
+
settled frame plus a verdict — no fixed sleeps, no second screenshot round trip.
|
|
5
|
+
"""
|
|
6
|
+
from .engine import (
|
|
7
|
+
BusyProbe,
|
|
8
|
+
Frame,
|
|
9
|
+
Grabber,
|
|
10
|
+
LatencyBook,
|
|
11
|
+
Region,
|
|
12
|
+
SettleConfig,
|
|
13
|
+
SettleResult,
|
|
14
|
+
act_and_settle,
|
|
15
|
+
browser_busy_probe,
|
|
16
|
+
install_browser_probe,
|
|
17
|
+
make_mss_grabber,
|
|
18
|
+
playwright_grabber,
|
|
19
|
+
wait_settled,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__version__ = "0.1.0a1"
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"BusyProbe",
|
|
26
|
+
"Frame",
|
|
27
|
+
"Grabber",
|
|
28
|
+
"LatencyBook",
|
|
29
|
+
"Region",
|
|
30
|
+
"SettleConfig",
|
|
31
|
+
"SettleResult",
|
|
32
|
+
"act_and_settle",
|
|
33
|
+
"browser_busy_probe",
|
|
34
|
+
"install_browser_probe",
|
|
35
|
+
"make_mss_grabber",
|
|
36
|
+
"playwright_grabber",
|
|
37
|
+
"wait_settled",
|
|
38
|
+
"__version__",
|
|
39
|
+
]
|