Skip to main content

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.

PyPI CI License: MIT

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, or on the picker a button opens (a hidden input the page clicks)
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
BROWSER_PATH Launch this Chromium executable instead: Brave, Arc, Chrome for Testing, a custom build
POINTCLICK_CDP Attach to a browser that is already running (http://localhost:9222) instead of launching one (below)
POINTCLICK_STATE A file (e.g. ~/.pointclick/state.json) that keeps cookies, localStorage and IndexedDB across restarts: log in once with show, stay logged in. Saved after every call, mode 600. Not sessionStorage. It holds your logins: never commit or share it; delete it to log out everywhere

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.

Your own browser

POINTCLICK_CDP attaches over the Chrome DevTools Protocol to a browser you started, so the agent works with that profile's cookies and logins. pointclick opens its own tab and leaves your other tabs alone; close() closes that tab and disconnects, and the browser keeps running.

"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
  --remote-debugging-port=9222 --user-data-dir="$HOME/.pointclick/chrome"
claude mcp add pointclick -e POINTCLICK_CDP=http://localhost:9222 -- uvx pointclick

Chrome 136 and later ignore --remote-debugging-port on the default profile, so give it a --user-data-dir of its own and log in there once. While attached, show does nothing (the window is already yours), device changes only the size and dark/light (scale and mobile need a browser pointclick launches), and POINTCLICK_STATE is never written: the profile already keeps the logins, and pointclick won't copy them to a file.

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, WAIT returns 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/extract backed 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().
  • POINTCLICK_CDP is the exception: the agent acts as you, signed in to everything that profile is, and so does anything else that can reach the debugging port. Keep the port on localhost and use a profile with only the logins the agent needs.
  • evaluate runs arbitrary JavaScript in the page; upload reads files you name from disk, and screenshot(path=...) writes one.

Limits

  • The list is viewport-only by default; long pages take SCROLL_DOWN or observe(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; evaluate covers some of it.

Development

uv sync --group dev
uv run pytest     # 69 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.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pointclick 0.4.0
File Size Uploaded Provenance Actions
pointclick-0.4.0.tar.gz 82.0 kB Without provenance Details for pointclick-0.4.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for pointclick 0.4.0
File Interpreter ABI Platform Actions
pointclick-0.4.0-py3-none-any.whl Python 3 none any Details for pointclick-0.4.0-py3-none-any.whl

Total release size: 101.9 kB

Release files / pointclick-0.4.0.tar.gz

Download URL pointclick-0.4.0.tar.gz
Size 82.0 kB
Tags Source
SHA-256 checksum
How to use checksums
02c5032698cc8f427228e9e707bd3337a5f584d7e4164ef301b8149786f1fba1
BLAKE2b-256 checksum
How to use checksums
43910de163b8ef7c89bbc073c2c0012deb1f8297e97be418885f115e3381d929
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","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.4.0-py3-none-any.whl

Download URL pointclick-0.4.0-py3-none-any.whl
Size 19.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2977863518474d9c6612a84dbe5dccf6e86e2ae66c77d8d447eb6a0fa80cfcda
BLAKE2b-256 checksum
How to use checksums
cfcab748afe46a4a8bcce4e369ca36596c523e8ca2cd9841b0d46c6db816ff3f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","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.4.0

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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page