pointclick 0.1.0__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.
- pointclick-0.1.0/.github/workflows/ci.yml +25 -0
- pointclick-0.1.0/.github/workflows/release.yml +19 -0
- pointclick-0.1.0/.gitignore +4 -0
- pointclick-0.1.0/LICENSE +21 -0
- pointclick-0.1.0/LICENSE-jev +21 -0
- pointclick-0.1.0/PKG-INFO +246 -0
- pointclick-0.1.0/README.md +225 -0
- pointclick-0.1.0/bench/agent-results-poll0.json +110 -0
- pointclick-0.1.0/bench/agent-results-poll1.json +110 -0
- pointclick-0.1.0/bench/agent_race.py +112 -0
- pointclick-0.1.0/bench/app/index.html +94 -0
- pointclick-0.1.0/bench/app/serve.py +42 -0
- pointclick-0.1.0/bench/jev_mcp.py +84 -0
- pointclick-0.1.0/bench/race.py +327 -0
- pointclick-0.1.0/bench/results-poll0.json +1226 -0
- pointclick-0.1.0/bench/results-poll1.json +1222 -0
- pointclick-0.1.0/pyproject.toml +41 -0
- pointclick-0.1.0/src/pointclick/__init__.py +1 -0
- pointclick-0.1.0/src/pointclick/server.py +421 -0
- pointclick-0.1.0/src/pointclick/snapshot.js +112 -0
- pointclick-0.1.0/tests/conftest.py +54 -0
- pointclick-0.1.0/tests/pages/basic.html +22 -0
- pointclick-0.1.0/tests/pages/child.html +4 -0
- pointclick-0.1.0/tests/pages/extra.html +15 -0
- pointclick-0.1.0/tests/pages/frames.html +6 -0
- pointclick-0.1.0/tests/pages/network.html +9 -0
- pointclick-0.1.0/tests/pages/popup.html +3 -0
- pointclick-0.1.0/tests/test_server.py +388 -0
- pointclick-0.1.0/tests/test_stdio.py +22 -0
- pointclick-0.1.0/uv.lock +847 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python: ["3.12", "3.13"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: astral-sh/setup-uv@v6
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ matrix.python }}
|
|
19
|
+
- run: uv sync --group dev
|
|
20
|
+
- run: uv run ruff check .
|
|
21
|
+
- run: uv run ruff format --check .
|
|
22
|
+
- run: uv run playwright install --with-deps chromium
|
|
23
|
+
- run: uv run pytest -q
|
|
24
|
+
env:
|
|
25
|
+
BROWSER_CHANNEL: chromium
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
pypi:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: pypi
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: astral-sh/setup-uv@v6
|
|
16
|
+
- name: Tag matches pyproject version
|
|
17
|
+
run: test "${GITHUB_REF_NAME#v}" = "$(uv version --short)"
|
|
18
|
+
- run: uv build
|
|
19
|
+
- run: uv publish --trusted-publishing always
|
pointclick-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dashgin
|
|
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, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Browser Use
|
|
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, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pointclick
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A browser MCP server with a small footprint: a numbered list of what you can click, and eight tools.
|
|
5
|
+
Project-URL: Homepage, https://github.com/dashgin/pointclick
|
|
6
|
+
Project-URL: Issues, https://github.com/dashgin/pointclick/issues
|
|
7
|
+
Author-email: Dashgin <dashgin@praxis.az>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
License-File: LICENSE-jev
|
|
11
|
+
Keywords: agent,automation,browser,mcp,playwright
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Software Development :: Testing
|
|
17
|
+
Requires-Python: >=3.12
|
|
18
|
+
Requires-Dist: mcp>=2
|
|
19
|
+
Requires-Dist: playwright>=1.50
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# pointclick
|
|
23
|
+
|
|
24
|
+
**A browser MCP server in under 1,000 tokens.** The page comes back as a numbered list of what
|
|
25
|
+
you can click, type into or select. Your agent points at a number; pointclick clicks it.
|
|
26
|
+
|
|
27
|
+
[](https://pypi.org/project/pointclick/)
|
|
28
|
+
[](https://github.com/dashgin/pointclick/actions/workflows/ci.yml)
|
|
29
|
+
[](LICENSE)
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
url: https://en.wikipedia.org/wiki/Main_Page
|
|
33
|
+
title: Wikipedia, the free encyclopedia
|
|
34
|
+
[1] link 'Wikipedia The Free Encyclopedia' <CLICK>
|
|
35
|
+
[2] searchbox 'Search Wikipedia' <TYPE,CLICK>
|
|
36
|
+
[3] button 'Search' <CLICK>
|
|
37
|
+
...
|
|
38
|
+
(more below: 3156px; SCROLL_DOWN or observe(full=true))
|
|
39
|
+
|
|
40
|
+
text:
|
|
41
|
+
Welcome to
|
|
42
|
+
Wikipedia
|
|
43
|
+
...
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
act("TYPE", "2", "Ada Lovelace") → the new list, once the page has settled
|
|
48
|
+
act("PRESS", "2", "Enter")
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Why
|
|
52
|
+
|
|
53
|
+
Same task, same model (Claude Sonnet 5 in headless Claude Code), one browser MCP each —
|
|
54
|
+
sign in, search, sort, open a record, read a number off it. 18 of 18 runs got it right.
|
|
55
|
+
|
|
56
|
+
| | turns | tokens read | cost per run | time |
|
|
57
|
+
|---|---|---|---|---|
|
|
58
|
+
| **pointclick** | **7** | **104k** | **$0.056** | **15.8 s** |
|
|
59
|
+
| Playwright MCP | 13 | 266k | $0.118 | 27.5 s |
|
|
60
|
+
| agent-browser | 12 | 525k | $0.223 | 17.8 s |
|
|
61
|
+
|
|
62
|
+
Medians. Cost is the API price Claude Code reports; time is noisy at this sample size.
|
|
63
|
+
|
|
64
|
+
- **Eight tools, under 1k tokens of definitions.** Playwright MCP's are ~5k, Chrome DevTools MCP's
|
|
65
|
+
~7k, agent-browser's ~18k. Clients that load every tool up front pay that on every turn.
|
|
66
|
+
- **Only what you can act on.** No wrapper `div`s, no layout tree: controls, their state, and the
|
|
67
|
+
visible text.
|
|
68
|
+
- **Every action returns the settled page.** No follow-up snapshot call, and no waiting for the
|
|
69
|
+
network to go quiet: long-polls and streams don't stall it.
|
|
70
|
+
|
|
71
|
+
## Quick start
|
|
72
|
+
|
|
73
|
+
Needs Python 3.12+ and [uv](https://docs.astral.sh/uv/). Uses your installed Chrome, or
|
|
74
|
+
Playwright's Chromium if there is none (`uvx --from pointclick playwright install chromium`).
|
|
75
|
+
|
|
76
|
+
**Claude Code**
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
claude mcp add pointclick -- uvx pointclick
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Cursor, Claude Desktop, Windsurf, and other clients** — add to the MCP config:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"mcpServers": {
|
|
87
|
+
"pointclick": { "command": "uvx", "args": ["pointclick"] }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**VS Code**
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
code --add-mcp '{"name":"pointclick","command":"uvx","args":["pointclick"]}'
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Codex**
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
codex mcp add pointclick -- uvx pointclick
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Your first prompt
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
Open news.ycombinator.com, go to the second page, and tell me the top story there.
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Tools
|
|
111
|
+
|
|
112
|
+
| Tool | |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `navigate(url)` | Open a page, return the list |
|
|
115
|
+
| `observe(full=false)` | The list again. Viewport only unless `full` |
|
|
116
|
+
| `act(operation, target, text)` | `CLICK` `TYPE` `SELECT` `PRESS` `HOVER` `SCROLL_DOWN` `SCROLL_UP` `WAIT` |
|
|
117
|
+
| `upload(target, paths)` | Set files on a file input |
|
|
118
|
+
| `screenshot(full_page=false)` | JPEG |
|
|
119
|
+
| `evaluate(js, max_chars=6000)` | Run JS in the page, JSON back. A longer result is cut and says so; `0` for all of it |
|
|
120
|
+
| `console()` | Console messages and page errors since the last call |
|
|
121
|
+
| `close()` | End the session; the next call starts fresh |
|
|
122
|
+
|
|
123
|
+
`target` is a number from the list (`"3"`, or `"3:2"` for the second option of a select), or any
|
|
124
|
+
Playwright selector (`"input[type=password]"`, `"text=Send"`, `"role=button[name='OK']"`).
|
|
125
|
+
Password and file inputs are never in the list; reach them by selector.
|
|
126
|
+
|
|
127
|
+
| Env | |
|
|
128
|
+
|---|---|
|
|
129
|
+
| `HEADED=1` | Show the window |
|
|
130
|
+
| `BROWSER_CHANNEL` | `chrome` (default), `msedge`, or `chromium` for Playwright's bundled build |
|
|
131
|
+
|
|
132
|
+
## How it works
|
|
133
|
+
|
|
134
|
+
- **The list** comes from one script that reads every visible control and the visible text in a
|
|
135
|
+
single pass — adapted from [jev-ultrafast](https://github.com/browser-use/jev-ultrafast)
|
|
136
|
+
(Browser Use × TypeSafe). It runs in every frame, cross-origin ones included, and a control that
|
|
137
|
+
only repeats its wrapper's label is listed once.
|
|
138
|
+
- **Settling.** After an action, pointclick returns once the page has been still for 150 ms and
|
|
139
|
+
no request younger than 0.5 s is pending — at most 2 s. Older requests (long-polls, streams)
|
|
140
|
+
don't hold it up. If the page is still busy after that, `WAIT` returns the moment it changes.
|
|
141
|
+
- **Numbers point at real elements**, not at a position. If a framework re-renders the element,
|
|
142
|
+
pointclick follows it when its label is unique on the page and says so; a duplicate label (two
|
|
143
|
+
"Start" buttons) is refused rather than guessed.
|
|
144
|
+
- **A timeout on a page that changed anyway** is reported that way, so the agent checks before
|
|
145
|
+
retrying instead of clicking twice.
|
|
146
|
+
- **Playwright underneath** handles selectors, uploads, popups (they become the current page),
|
|
147
|
+
screenshots and the console. Every session is a fresh, isolated browser context.
|
|
148
|
+
|
|
149
|
+
`server.py` is ~350 lines and `snapshot.js` ~110. Small enough to read before you trust it.
|
|
150
|
+
|
|
151
|
+
## Benchmarks
|
|
152
|
+
|
|
153
|
+
`bench/` has a small SPA shaped like a real dashboard — demo login, a list with search and sort, a
|
|
154
|
+
detail page, API calls over the network, and an optional long-poll like a realtime fallback.
|
|
155
|
+
|
|
156
|
+
**With a real agent** — headless Claude Code, built-in tools off, the task given in plain words:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
uv run python bench/agent_race.py --runs 3 --model sonnet
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
| | correct | turns | tokens read | tokens written | cost | time, no long-poll | time, long-poll open |
|
|
163
|
+
|---|---|---|---|---|---|---|---|
|
|
164
|
+
| **pointclick** | 6/6 | **7** | **104k** | **620–660** | **$0.056** | **13.7 s** | 16.7 s |
|
|
165
|
+
| Playwright MCP 0.0.81 | 6/6 | 13 | 266–289k | 1.1k | $0.113–0.118 | 23.3 s | 28.5 s |
|
|
166
|
+
| agent-browser 0.38.1 | 6/6 | 12 | 525k | 760–780 | $0.223 | 19.1 s | **14.8 s** |
|
|
167
|
+
|
|
168
|
+
Medians of 3 runs per column. Turns and tokens barely move between runs; time moves by up to a
|
|
169
|
+
third with model latency, so read it as a range. Playwright MCP's action replies link to a
|
|
170
|
+
snapshot file instead of including the page, so the model asks for a snapshot after each step.
|
|
171
|
+
agent-browser's actions return nothing, so every step is an action plus a snapshot too.
|
|
172
|
+
|
|
173
|
+
**Tools alone** — the same flow driven by a script, so no model time is included:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
uv run python bench/race.py --runs 8 --poll 0
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
| | calls | text to the model | tool time | with a long-poll open |
|
|
180
|
+
|---|---|---|---|---|
|
|
181
|
+
| **pointclick** | **6** | **6.6k chars (~1.9k tokens)** | 2.25 s | **2.31 s** |
|
|
182
|
+
| Playwright MCP 0.0.81 | 6 | 17.0k chars (~4.9k tokens) | 3.32 s | 8.27 s |
|
|
183
|
+
| agent-browser 0.38.1 | 18 | 12.3k chars (~3.5k tokens) | **1.13 s** | 1.12 s |
|
|
184
|
+
|
|
185
|
+
agent-browser's tool time is lowest because its actions return nothing: it re-snapshots, and the
|
|
186
|
+
script polls for free. In a real agent every one of those calls is a model turn.
|
|
187
|
+
|
|
188
|
+
**Tool definitions**, sent with every request by clients that don't load tools lazily (all four
|
|
189
|
+
estimated the same way, JSON characters ÷ 3.5):
|
|
190
|
+
|
|
191
|
+
| | tools | definitions |
|
|
192
|
+
|---|---|---|
|
|
193
|
+
| **pointclick** | **8** | **~0.8k tokens** |
|
|
194
|
+
| Playwright MCP | 26 | ~5.2k tokens |
|
|
195
|
+
| Chrome DevTools MCP | 29 | ~6.9k tokens |
|
|
196
|
+
| agent-browser (`core`) | 29 | ~18.4k tokens |
|
|
197
|
+
|
|
198
|
+
Measured on an M-series Mac with Chrome 153. Raw results are in `bench/*.json`.
|
|
199
|
+
|
|
200
|
+
## When to use something else
|
|
201
|
+
|
|
202
|
+
- **Playwright MCP** — you need network interception, tracing, PDF export, or the full Playwright
|
|
203
|
+
surface.
|
|
204
|
+
- **Chrome DevTools MCP** — performance traces, Lighthouse-style audits, deep DevTools debugging.
|
|
205
|
+
- **agent-browser** — you'd rather drive the browser from a shell (CLI + skills), want saved auth
|
|
206
|
+
profiles, or need its long tail of commands.
|
|
207
|
+
- **Stagehand** — you want natural-language `act`/`extract` backed by its own model, or hosted
|
|
208
|
+
browsers on Browserbase.
|
|
209
|
+
|
|
210
|
+
pointclick does one job: get an agent through a web app with as little text and as few turns as
|
|
211
|
+
it can — locally, with no account and no second model.
|
|
212
|
+
|
|
213
|
+
## Security
|
|
214
|
+
|
|
215
|
+
- Everything on a page — text, labels, titles — is **untrusted input** to your model. A page can
|
|
216
|
+
try to instruct your agent. Keep the agent's permissions narrow and confirm consequential steps.
|
|
217
|
+
- Each session is an isolated browser context: no access to your Chrome profile, cookies or
|
|
218
|
+
passwords. Nothing persists after `close()`.
|
|
219
|
+
- `evaluate` runs arbitrary JavaScript in the page; `upload` reads files you name from disk.
|
|
220
|
+
|
|
221
|
+
## Limits
|
|
222
|
+
|
|
223
|
+
- The list is viewport-only by default; long pages take `SCROLL_DOWN` or `observe(full=true)`.
|
|
224
|
+
- Every action returns the whole list, not a diff.
|
|
225
|
+
- Chromium only, one page at a time per server. Calls sent in parallel run one after another.
|
|
226
|
+
- No network inspection, PDF export or dialog control; `evaluate` covers some of it.
|
|
227
|
+
|
|
228
|
+
## Development
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
uv sync --group dev
|
|
232
|
+
uv run pytest # 45 tests; fixture pages served from two local origins
|
|
233
|
+
uv run ruff check . && uv run ruff format --check .
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
To release, bump `version` in `pyproject.toml`, then push a matching tag (`v0.1.0`). `release.yml` publishes
|
|
237
|
+
to PyPI through trusted publishing; there is no token to keep.
|
|
238
|
+
|
|
239
|
+
## Credits
|
|
240
|
+
|
|
241
|
+
The page snapshot is adapted from [jev-ultrafast](https://github.com/browser-use/jev-ultrafast),
|
|
242
|
+
MIT © 2026 Browser Use — see `LICENSE-jev`.
|
|
243
|
+
|
|
244
|
+
## License
|
|
245
|
+
|
|
246
|
+
MIT
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# pointclick
|
|
2
|
+
|
|
3
|
+
**A browser MCP server in under 1,000 tokens.** The page comes back as a numbered list of what
|
|
4
|
+
you can click, type into or select. Your agent points at a number; pointclick clicks it.
|
|
5
|
+
|
|
6
|
+
[](https://pypi.org/project/pointclick/)
|
|
7
|
+
[](https://github.com/dashgin/pointclick/actions/workflows/ci.yml)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
url: https://en.wikipedia.org/wiki/Main_Page
|
|
12
|
+
title: Wikipedia, the free encyclopedia
|
|
13
|
+
[1] link 'Wikipedia The Free Encyclopedia' <CLICK>
|
|
14
|
+
[2] searchbox 'Search Wikipedia' <TYPE,CLICK>
|
|
15
|
+
[3] button 'Search' <CLICK>
|
|
16
|
+
...
|
|
17
|
+
(more below: 3156px; SCROLL_DOWN or observe(full=true))
|
|
18
|
+
|
|
19
|
+
text:
|
|
20
|
+
Welcome to
|
|
21
|
+
Wikipedia
|
|
22
|
+
...
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
act("TYPE", "2", "Ada Lovelace") → the new list, once the page has settled
|
|
27
|
+
act("PRESS", "2", "Enter")
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Why
|
|
31
|
+
|
|
32
|
+
Same task, same model (Claude Sonnet 5 in headless Claude Code), one browser MCP each —
|
|
33
|
+
sign in, search, sort, open a record, read a number off it. 18 of 18 runs got it right.
|
|
34
|
+
|
|
35
|
+
| | turns | tokens read | cost per run | time |
|
|
36
|
+
|---|---|---|---|---|
|
|
37
|
+
| **pointclick** | **7** | **104k** | **$0.056** | **15.8 s** |
|
|
38
|
+
| Playwright MCP | 13 | 266k | $0.118 | 27.5 s |
|
|
39
|
+
| agent-browser | 12 | 525k | $0.223 | 17.8 s |
|
|
40
|
+
|
|
41
|
+
Medians. Cost is the API price Claude Code reports; time is noisy at this sample size.
|
|
42
|
+
|
|
43
|
+
- **Eight tools, under 1k tokens of definitions.** Playwright MCP's are ~5k, Chrome DevTools MCP's
|
|
44
|
+
~7k, agent-browser's ~18k. Clients that load every tool up front pay that on every turn.
|
|
45
|
+
- **Only what you can act on.** No wrapper `div`s, no layout tree: controls, their state, and the
|
|
46
|
+
visible text.
|
|
47
|
+
- **Every action returns the settled page.** No follow-up snapshot call, and no waiting for the
|
|
48
|
+
network to go quiet: long-polls and streams don't stall it.
|
|
49
|
+
|
|
50
|
+
## Quick start
|
|
51
|
+
|
|
52
|
+
Needs Python 3.12+ and [uv](https://docs.astral.sh/uv/). Uses your installed Chrome, or
|
|
53
|
+
Playwright's Chromium if there is none (`uvx --from pointclick playwright install chromium`).
|
|
54
|
+
|
|
55
|
+
**Claude Code**
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
claude mcp add pointclick -- uvx pointclick
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Cursor, Claude Desktop, Windsurf, and other clients** — add to the MCP config:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"mcpServers": {
|
|
66
|
+
"pointclick": { "command": "uvx", "args": ["pointclick"] }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**VS Code**
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
code --add-mcp '{"name":"pointclick","command":"uvx","args":["pointclick"]}'
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Codex**
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
codex mcp add pointclick -- uvx pointclick
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Your first prompt
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
Open news.ycombinator.com, go to the second page, and tell me the top story there.
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Tools
|
|
90
|
+
|
|
91
|
+
| Tool | |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `navigate(url)` | Open a page, return the list |
|
|
94
|
+
| `observe(full=false)` | The list again. Viewport only unless `full` |
|
|
95
|
+
| `act(operation, target, text)` | `CLICK` `TYPE` `SELECT` `PRESS` `HOVER` `SCROLL_DOWN` `SCROLL_UP` `WAIT` |
|
|
96
|
+
| `upload(target, paths)` | Set files on a file input |
|
|
97
|
+
| `screenshot(full_page=false)` | JPEG |
|
|
98
|
+
| `evaluate(js, max_chars=6000)` | Run JS in the page, JSON back. A longer result is cut and says so; `0` for all of it |
|
|
99
|
+
| `console()` | Console messages and page errors since the last call |
|
|
100
|
+
| `close()` | End the session; the next call starts fresh |
|
|
101
|
+
|
|
102
|
+
`target` is a number from the list (`"3"`, or `"3:2"` for the second option of a select), or any
|
|
103
|
+
Playwright selector (`"input[type=password]"`, `"text=Send"`, `"role=button[name='OK']"`).
|
|
104
|
+
Password and file inputs are never in the list; reach them by selector.
|
|
105
|
+
|
|
106
|
+
| Env | |
|
|
107
|
+
|---|---|
|
|
108
|
+
| `HEADED=1` | Show the window |
|
|
109
|
+
| `BROWSER_CHANNEL` | `chrome` (default), `msedge`, or `chromium` for Playwright's bundled build |
|
|
110
|
+
|
|
111
|
+
## How it works
|
|
112
|
+
|
|
113
|
+
- **The list** comes from one script that reads every visible control and the visible text in a
|
|
114
|
+
single pass — adapted from [jev-ultrafast](https://github.com/browser-use/jev-ultrafast)
|
|
115
|
+
(Browser Use × TypeSafe). It runs in every frame, cross-origin ones included, and a control that
|
|
116
|
+
only repeats its wrapper's label is listed once.
|
|
117
|
+
- **Settling.** After an action, pointclick returns once the page has been still for 150 ms and
|
|
118
|
+
no request younger than 0.5 s is pending — at most 2 s. Older requests (long-polls, streams)
|
|
119
|
+
don't hold it up. If the page is still busy after that, `WAIT` returns the moment it changes.
|
|
120
|
+
- **Numbers point at real elements**, not at a position. If a framework re-renders the element,
|
|
121
|
+
pointclick follows it when its label is unique on the page and says so; a duplicate label (two
|
|
122
|
+
"Start" buttons) is refused rather than guessed.
|
|
123
|
+
- **A timeout on a page that changed anyway** is reported that way, so the agent checks before
|
|
124
|
+
retrying instead of clicking twice.
|
|
125
|
+
- **Playwright underneath** handles selectors, uploads, popups (they become the current page),
|
|
126
|
+
screenshots and the console. Every session is a fresh, isolated browser context.
|
|
127
|
+
|
|
128
|
+
`server.py` is ~350 lines and `snapshot.js` ~110. Small enough to read before you trust it.
|
|
129
|
+
|
|
130
|
+
## Benchmarks
|
|
131
|
+
|
|
132
|
+
`bench/` has a small SPA shaped like a real dashboard — demo login, a list with search and sort, a
|
|
133
|
+
detail page, API calls over the network, and an optional long-poll like a realtime fallback.
|
|
134
|
+
|
|
135
|
+
**With a real agent** — headless Claude Code, built-in tools off, the task given in plain words:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
uv run python bench/agent_race.py --runs 3 --model sonnet
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
| | correct | turns | tokens read | tokens written | cost | time, no long-poll | time, long-poll open |
|
|
142
|
+
|---|---|---|---|---|---|---|---|
|
|
143
|
+
| **pointclick** | 6/6 | **7** | **104k** | **620–660** | **$0.056** | **13.7 s** | 16.7 s |
|
|
144
|
+
| Playwright MCP 0.0.81 | 6/6 | 13 | 266–289k | 1.1k | $0.113–0.118 | 23.3 s | 28.5 s |
|
|
145
|
+
| agent-browser 0.38.1 | 6/6 | 12 | 525k | 760–780 | $0.223 | 19.1 s | **14.8 s** |
|
|
146
|
+
|
|
147
|
+
Medians of 3 runs per column. Turns and tokens barely move between runs; time moves by up to a
|
|
148
|
+
third with model latency, so read it as a range. Playwright MCP's action replies link to a
|
|
149
|
+
snapshot file instead of including the page, so the model asks for a snapshot after each step.
|
|
150
|
+
agent-browser's actions return nothing, so every step is an action plus a snapshot too.
|
|
151
|
+
|
|
152
|
+
**Tools alone** — the same flow driven by a script, so no model time is included:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
uv run python bench/race.py --runs 8 --poll 0
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
| | calls | text to the model | tool time | with a long-poll open |
|
|
159
|
+
|---|---|---|---|---|
|
|
160
|
+
| **pointclick** | **6** | **6.6k chars (~1.9k tokens)** | 2.25 s | **2.31 s** |
|
|
161
|
+
| Playwright MCP 0.0.81 | 6 | 17.0k chars (~4.9k tokens) | 3.32 s | 8.27 s |
|
|
162
|
+
| agent-browser 0.38.1 | 18 | 12.3k chars (~3.5k tokens) | **1.13 s** | 1.12 s |
|
|
163
|
+
|
|
164
|
+
agent-browser's tool time is lowest because its actions return nothing: it re-snapshots, and the
|
|
165
|
+
script polls for free. In a real agent every one of those calls is a model turn.
|
|
166
|
+
|
|
167
|
+
**Tool definitions**, sent with every request by clients that don't load tools lazily (all four
|
|
168
|
+
estimated the same way, JSON characters ÷ 3.5):
|
|
169
|
+
|
|
170
|
+
| | tools | definitions |
|
|
171
|
+
|---|---|---|
|
|
172
|
+
| **pointclick** | **8** | **~0.8k tokens** |
|
|
173
|
+
| Playwright MCP | 26 | ~5.2k tokens |
|
|
174
|
+
| Chrome DevTools MCP | 29 | ~6.9k tokens |
|
|
175
|
+
| agent-browser (`core`) | 29 | ~18.4k tokens |
|
|
176
|
+
|
|
177
|
+
Measured on an M-series Mac with Chrome 153. Raw results are in `bench/*.json`.
|
|
178
|
+
|
|
179
|
+
## When to use something else
|
|
180
|
+
|
|
181
|
+
- **Playwright MCP** — you need network interception, tracing, PDF export, or the full Playwright
|
|
182
|
+
surface.
|
|
183
|
+
- **Chrome DevTools MCP** — performance traces, Lighthouse-style audits, deep DevTools debugging.
|
|
184
|
+
- **agent-browser** — you'd rather drive the browser from a shell (CLI + skills), want saved auth
|
|
185
|
+
profiles, or need its long tail of commands.
|
|
186
|
+
- **Stagehand** — you want natural-language `act`/`extract` backed by its own model, or hosted
|
|
187
|
+
browsers on Browserbase.
|
|
188
|
+
|
|
189
|
+
pointclick does one job: get an agent through a web app with as little text and as few turns as
|
|
190
|
+
it can — locally, with no account and no second model.
|
|
191
|
+
|
|
192
|
+
## Security
|
|
193
|
+
|
|
194
|
+
- Everything on a page — text, labels, titles — is **untrusted input** to your model. A page can
|
|
195
|
+
try to instruct your agent. Keep the agent's permissions narrow and confirm consequential steps.
|
|
196
|
+
- Each session is an isolated browser context: no access to your Chrome profile, cookies or
|
|
197
|
+
passwords. Nothing persists after `close()`.
|
|
198
|
+
- `evaluate` runs arbitrary JavaScript in the page; `upload` reads files you name from disk.
|
|
199
|
+
|
|
200
|
+
## Limits
|
|
201
|
+
|
|
202
|
+
- The list is viewport-only by default; long pages take `SCROLL_DOWN` or `observe(full=true)`.
|
|
203
|
+
- Every action returns the whole list, not a diff.
|
|
204
|
+
- Chromium only, one page at a time per server. Calls sent in parallel run one after another.
|
|
205
|
+
- No network inspection, PDF export or dialog control; `evaluate` covers some of it.
|
|
206
|
+
|
|
207
|
+
## Development
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
uv sync --group dev
|
|
211
|
+
uv run pytest # 45 tests; fixture pages served from two local origins
|
|
212
|
+
uv run ruff check . && uv run ruff format --check .
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
To release, bump `version` in `pyproject.toml`, then push a matching tag (`v0.1.0`). `release.yml` publishes
|
|
216
|
+
to PyPI through trusted publishing; there is no token to keep.
|
|
217
|
+
|
|
218
|
+
## Credits
|
|
219
|
+
|
|
220
|
+
The page snapshot is adapted from [jev-ultrafast](https://github.com/browser-use/jev-ultrafast),
|
|
221
|
+
MIT © 2026 Browser Use — see `LICENSE-jev`.
|
|
222
|
+
|
|
223
|
+
## License
|
|
224
|
+
|
|
225
|
+
MIT
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"run": 0,
|
|
4
|
+
"tool": "pointclick",
|
|
5
|
+
"ok": true,
|
|
6
|
+
"answer": "14",
|
|
7
|
+
"s": 17.4,
|
|
8
|
+
"wall_s": 22.3,
|
|
9
|
+
"turns": 7,
|
|
10
|
+
"context_tokens": 88417,
|
|
11
|
+
"output_tokens": 646,
|
|
12
|
+
"cost_usd": 0.0725808
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"run": 0,
|
|
16
|
+
"tool": "playwright",
|
|
17
|
+
"ok": true,
|
|
18
|
+
"answer": "14",
|
|
19
|
+
"s": 31.3,
|
|
20
|
+
"wall_s": 36.3,
|
|
21
|
+
"turns": 13,
|
|
22
|
+
"context_tokens": 263170,
|
|
23
|
+
"output_tokens": 1143,
|
|
24
|
+
"cost_usd": 0.1277116
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"run": 0,
|
|
28
|
+
"tool": "agent-browser",
|
|
29
|
+
"ok": true,
|
|
30
|
+
"answer": "14",
|
|
31
|
+
"s": 30.2,
|
|
32
|
+
"wall_s": 34.5,
|
|
33
|
+
"turns": 12,
|
|
34
|
+
"context_tokens": 525561,
|
|
35
|
+
"output_tokens": 879,
|
|
36
|
+
"cost_usd": 0.24370040000000004
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"run": 1,
|
|
40
|
+
"tool": "playwright",
|
|
41
|
+
"ok": true,
|
|
42
|
+
"answer": "14",
|
|
43
|
+
"s": 23.3,
|
|
44
|
+
"wall_s": 28.0,
|
|
45
|
+
"turns": 14,
|
|
46
|
+
"context_tokens": 292592,
|
|
47
|
+
"output_tokens": 1140,
|
|
48
|
+
"cost_usd": 0.11849500000000002
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"run": 1,
|
|
52
|
+
"tool": "agent-browser",
|
|
53
|
+
"ok": true,
|
|
54
|
+
"answer": "14",
|
|
55
|
+
"s": 19.1,
|
|
56
|
+
"wall_s": 24.0,
|
|
57
|
+
"turns": 12,
|
|
58
|
+
"context_tokens": 524845,
|
|
59
|
+
"output_tokens": 756,
|
|
60
|
+
"cost_usd": 0.22254819999999997
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"run": 1,
|
|
64
|
+
"tool": "pointclick",
|
|
65
|
+
"ok": true,
|
|
66
|
+
"answer": "14",
|
|
67
|
+
"s": 13.7,
|
|
68
|
+
"wall_s": 18.1,
|
|
69
|
+
"turns": 7,
|
|
70
|
+
"context_tokens": 104512,
|
|
71
|
+
"output_tokens": 620,
|
|
72
|
+
"cost_usd": 0.055791
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"run": 2,
|
|
76
|
+
"tool": "agent-browser",
|
|
77
|
+
"ok": true,
|
|
78
|
+
"answer": "14",
|
|
79
|
+
"s": 16.4,
|
|
80
|
+
"wall_s": 20.9,
|
|
81
|
+
"turns": 12,
|
|
82
|
+
"context_tokens": 525054,
|
|
83
|
+
"output_tokens": 780,
|
|
84
|
+
"cost_usd": 0.22290980000000002
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"run": 2,
|
|
88
|
+
"tool": "pointclick",
|
|
89
|
+
"ok": true,
|
|
90
|
+
"answer": "14",
|
|
91
|
+
"s": 13.4,
|
|
92
|
+
"wall_s": 17.8,
|
|
93
|
+
"turns": 7,
|
|
94
|
+
"context_tokens": 104249,
|
|
95
|
+
"output_tokens": 581,
|
|
96
|
+
"cost_usd": 0.0552002
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"run": 2,
|
|
100
|
+
"tool": "playwright",
|
|
101
|
+
"ok": true,
|
|
102
|
+
"answer": "14",
|
|
103
|
+
"s": 21.7,
|
|
104
|
+
"wall_s": 26.4,
|
|
105
|
+
"turns": 13,
|
|
106
|
+
"context_tokens": 265844,
|
|
107
|
+
"output_tokens": 1038,
|
|
108
|
+
"cost_usd": 0.11100080000000001
|
|
109
|
+
}
|
|
110
|
+
]
|