pointclick
A browser MCP server in under 1,000 tokens. The page comes back as a numbered list of what you can click, type into or select. Your agent points at a number; pointclick clicks it.
url: https://en.wikipedia.org/wiki/Main_Page
title: Wikipedia, the free encyclopedia
[1] link 'Wikipedia The Free Encyclopedia' <CLICK>
[2] searchbox 'Search Wikipedia' <TYPE,CLICK>
[3] button 'Search' <CLICK>
...
(more below: 3156px; SCROLL_DOWN or observe(full=true))
text:
Welcome to
Wikipedia
...
act("TYPE", "2", "Ada Lovelace") → the new list, once the page has settled
act("PRESS", "2", "Enter")
Why
Same task, same model (Claude Sonnet 5 in headless Claude Code), one browser MCP each — sign in, search, sort, open a record, read a number off it. 18 of 18 runs got it right.
| turns | tokens read | cost per run | time | |
|---|---|---|---|---|
| pointclick | 7 | 104k | $0.056 | 15.8 s |
| Playwright MCP | 13 | 266k | $0.118 | 27.5 s |
| agent-browser | 12 | 525k | $0.223 | 17.8 s |
Medians. Cost is the API price Claude Code reports; time is noisy at this sample size.
- Nine tools, under 1k tokens of definitions. Playwright MCP's are ~5k, Chrome DevTools MCP's ~7k, agent-browser's ~18k. Clients that load every tool up front pay that on every turn.
- Only what you can act on. No wrapper
divs, no layout tree: controls, their state, and the visible text. - Every action returns the settled page. No follow-up snapshot call, and no waiting for the network to go quiet: long-polls and streams don't stall it.
Quick start
Needs Python 3.12+ and uv. Uses your installed Chrome, or
Playwright's Chromium if there is none (uvx --from pointclick playwright install chromium).
Claude Code
claude mcp add pointclick -- uvx pointclick
Cursor, Claude Desktop, Windsurf, and other clients — add to the MCP config:
{
"mcpServers": {
"pointclick": { "command": "uvx", "args": ["pointclick"] }
}
}
VS Code
code --add-mcp '{"name":"pointclick","command":"uvx","args":["pointclick"]}'
Codex
codex mcp add pointclick -- uvx pointclick
Your first prompt
Open news.ycombinator.com, go to the second page, and tell me the top story there.
Tools
| Tool | |
|---|---|
navigate(url, device) |
Open a page, return the list. device switches the screen first (below) |
observe(full=false) |
The list again. Viewport only unless full |
act(operation, target, text) |
CLICK TYPE SELECT PRESS HOVER SCROLL_DOWN SCROLL_UP WAIT |
upload(target, paths) |
Set files on a file input |
screenshot(full_page, format, path) |
JPEG inline, or format="png". With path: the file is written there and only its path and pixel size come back |
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 |
console() |
Console messages and page errors since the last call |
show(visible=true) |
Open the window so a person can log in or pass a check, then show(false) to hide it. Same cookies, storage, device and page |
close() |
End the session; the next call starts fresh |
target is a number from the list ("3", or "3:2" for the second option of a select), or any
Playwright selector ("input[type=password]", "text=Send", "role=button[name='OK']").
Password and file inputs are never in the list; reach them by selector.
| Env | |
|---|---|
HEADED=1 |
Start with the window showing (show switches it at any time) |
BROWSER_CHANNEL |
chrome (default), msedge, or chromium for Playwright's bundled build |
Store screenshots
The screen is 1280×800 at scale 1 until navigate is given a device: "WxH@scale", plus
" mobile" for a phone (touch, and the page's <meta viewport> is honoured), plus " dark" or
" light" for the colour scheme. "dark" alone switches the scheme and keeps the size.
navigate("http://localhost:8081/home", device="440x956@3 mobile")
screenshot(path="shots/iphone-6.9/home.png") → /abs/shots/iphone-6.9/home.png 1320x2868 png
Scale can't change on a live browser context, so a new device opens a new one, and show
relaunches the browser. Cookies, localStorage, IndexedDB and the current page's sessionStorage
(where some apps keep their login) carry over; other tabs and history don't. Saving to a
path writes PNG (JPEG for .jpg), creates missing folders and keeps the image out of the
agent's context, so a batch of captures costs a line each. close() goes back to 1280×800.
How it works
- The list comes from one script that reads every visible control and the visible text in a single pass — adapted from jev-ultrafast (Browser Use × TypeSafe). It runs in every frame, cross-origin ones included, and a control that only repeats its wrapper's label is listed once.
- Settling. After an action, pointclick returns once the page has been still for 150 ms and
no request younger than 0.5 s is pending — at most 2 s. Older requests (long-polls, streams)
don't hold it up. If the page is still busy after that,
WAITreturns the moment it changes. - Numbers point at real elements, not at a position. If a framework re-renders the element, pointclick follows it when its label is unique on the page and says so; a duplicate label (two "Start" buttons) is refused rather than guessed.
- A timeout on a page that changed anyway is reported that way, so the agent checks before retrying instead of clicking twice.
- Playwright underneath handles selectors, uploads, popups (they become the current page), screenshots and the console. Every session is a fresh, isolated browser context.
server.py is ~400 lines and snapshot.js ~110. Small enough to read before you trust it.
Benchmarks
bench/ has a small SPA shaped like a real dashboard — demo login, a list with search and sort, a
detail page, API calls over the network, and an optional long-poll like a realtime fallback.
With a real agent — headless Claude Code, built-in tools off, the task given in plain words:
uv run python bench/agent_race.py --runs 3 --model sonnet
| correct | turns | tokens read | tokens written | cost | time, no long-poll | time, long-poll open | |
|---|---|---|---|---|---|---|---|
| pointclick | 6/6 | 7 | 104k | 620–660 | $0.056 | 13.7 s | 16.7 s |
| Playwright MCP 0.0.81 | 6/6 | 13 | 266–289k | 1.1k | $0.113–0.118 | 23.3 s | 28.5 s |
| agent-browser 0.38.1 | 6/6 | 12 | 525k | 760–780 | $0.223 | 19.1 s | 14.8 s |
Medians of 3 runs per column. Turns and tokens barely move between runs; time moves by up to a third with model latency, so read it as a range. Playwright MCP's action replies link to a snapshot file instead of including the page, so the model asks for a snapshot after each step. agent-browser's actions return nothing, so every step is an action plus a snapshot too.
Tools alone — the same flow driven by a script, so no model time is included:
uv run python bench/race.py --runs 8 --poll 0
| calls | text to the model | tool time | with a long-poll open | |
|---|---|---|---|---|
| pointclick | 6 | 6.6k chars (~1.9k tokens) | 2.25 s | 2.31 s |
| Playwright MCP 0.0.81 | 6 | 17.0k chars (~4.9k tokens) | 3.32 s | 8.27 s |
| agent-browser 0.38.1 | 18 | 12.3k chars (~3.5k tokens) | 1.13 s | 1.12 s |
agent-browser's tool time is lowest because its actions return nothing: it re-snapshots, and the script polls for free. In a real agent every one of those calls is a model turn.
Tool definitions, sent with every request by clients that don't load tools lazily (all four estimated the same way, JSON characters ÷ 3.5):
| tools | definitions | |
|---|---|---|
| pointclick | 9 | ~1.0k tokens |
| Playwright MCP | 26 | ~5.2k tokens |
| Chrome DevTools MCP | 29 | ~6.9k tokens |
agent-browser (core) |
29 | ~18.4k tokens |
Measured on an M-series Mac with Chrome 153. Raw results are in bench/*.json.
When to use something else
- Playwright MCP — you need network interception, tracing, PDF export, or the full Playwright surface.
- Chrome DevTools MCP — performance traces, Lighthouse-style audits, deep DevTools debugging.
- agent-browser — you'd rather drive the browser from a shell (CLI + skills), want saved auth profiles, or need its long tail of commands.
- Stagehand — you want natural-language
act/extractbacked by its own model, or hosted browsers on Browserbase.
pointclick does one job: get an agent through a web app with as little text and as few turns as it can — locally, with no account and no second model.
Security
- Everything on a page — text, labels, titles — is untrusted input to your model. A page can try to instruct your agent. Keep the agent's permissions narrow and confirm consequential steps.
- Each session is an isolated browser context: no access to your Chrome profile, cookies or
passwords. Nothing persists after
close(). evaluateruns arbitrary JavaScript in the page;uploadreads files you name from disk, andscreenshot(path=...)writes one.
Limits
- The list is viewport-only by default; long pages take
SCROLL_DOWNorobserve(full=true). - Every action returns the whole list, not a diff.
- Chromium only, one page at a time per server. Calls sent in parallel run one after another.
- No network inspection, PDF export or dialog control;
evaluatecovers some of it.
Development
uv sync --group dev
uv run pytest # 53 tests; fixture pages served from two local origins
uv run ruff check . && uv run ruff format --check .
To release, bump version in pyproject.toml, then push a matching tag (v0.1.0). release.yml publishes
to PyPI through trusted publishing; there is no token to keep.
Credits
The page snapshot is adapted from jev-ultrafast,
MIT © 2026 Browser Use — see LICENSE-jev.
License
MIT
Metadata
Release files for pointclick 0.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| pointclick-0.3.1.tar.gz | 78.2 kB | Without provenance | Details for pointclick-0.3.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| pointclick-0.3.1-py3-none-any.whl | Python 3 | none | any | Details for pointclick-0.3.1-py3-none-any.whl |
Total release size: 95.9 kB
Release files / pointclick-0.3.1.tar.gz
| Download URL | pointclick-0.3.1.tar.gz |
|---|---|
| Size | 78.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ea51b035a9ba77cd019d41d41e185ae75dc85b3fca255e61696e2bbed0cfc67f
|
|
BLAKE2b-256 checksum How to use checksums |
f7ac8d47ce6132cca342fc0d1619571342d3a8e5531914a8072c59682b8546aa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / pointclick-0.3.1-py3-none-any.whl
| Download URL | pointclick-0.3.1-py3-none-any.whl |
|---|---|
| Size | 17.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
25e56998ff312919a6f0eb107f1df362c4bb671670fb5060c589ac02d5d24a0f
|
|
BLAKE2b-256 checksum How to use checksums |
46bdc0a8b6c91d5fcb4f7220b927d5bea4ea7266f4d04464a2e334247e627c93
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for pointclick 0.3.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.