Skip to main content

nekoro-browser — browser automation CLI + MCP server

tests PyPI Python versions MIT License MCP supported

Let your AI coding tool drive your own Chrome — open pages, click, read them back, still logged in everywhere.

中文

Quick Start · Examples · Compare · MCP · API · Architecture · Site Knowledge · Limitations · Reference


nekoro-browser drives the Chrome you already use — logins, cookies, sessions, all intact.

Other automation tools spawn a fresh browser: no logins, nothing works. nekoro-browser just adds a small extension: same profile, no second instance, no "controlled by automated software" banner.

[!NOTE] Install is one uv tool install — Python stdlib only, no bundled engine, no 200MB download.

Every helper is reflected into an MCP tool (53 of them), so Claude Code, Cursor, Cline, opencode, Codex and VS Code/Copilot can drive the browser directly. Bring any model — no subscription, no lock-in. MIT, extension source included.

Quick Start

Rather have your AI do it? Paste this into Claude Code / Cursor / opencode
Install nekoro-browser for me:
1. `uv tool install nekoro-browser` (no uv → `pipx install nekoro-browser`).
2. Run `nekoro-browser setup` and show me the extension path it prints. This step is mine:
   I open chrome://extensions, turn on Developer mode, click Load unpacked, paste that
   path. Wait until I say it's loaded — you cannot click this for me.
3. Then start the daemon in a separate terminal that stays open: `nekoro-browser`.
4. Last step depends on how I'll use it — ask me which:
   - from my AI editor → register the MCP server (`claude mcp add nekoro-browser --
     nekoro-browser-mcp`, or the equivalent config for my client), then I restart it;
   - from the terminal only → nothing to do, `echo "page_info()" | nekoro-browser` works.
5. Finish with `nekoro-browser --doctor` and tell me if daemon / extension / service
   worker are all green.

1 — Install (Python 3.12+, zero third-party dependencies)

uv tool install nekoro-browser

No uv? pipx install nekoro-browser works too.

From source: git clone https://github.com/zeshuochen/nekoro-browser && cd nekoro-browser && uv pip install -e .

[!WARNING] Upgrading? uv tool upgrade nekoro-browser only updates the Python side — reload the extension afterwards: nekoro-browser --reload-ext (or Reload on the card in chrome://extensions).

2 — Load the extension

nekoro-browser setup

Copies the extension directory to your clipboard and waits until it connects. Meanwhile: chrome://extensions/Developer modeLoad unpacked → paste.

3 — Start the daemon — open a second terminal and leave it running (it's the background process that holds the Chrome connection; close it and everything stops)

nekoro-browser

4 — Drive the browser. Pick the way you actually work:

From your AI coding tool (MCP) — one command for Claude Code, other clients in MCP:

claude mcp add nekoro-browser -- nekoro-browser-mcp

Restart the client and ask it to open a page. That's it — 53 browser tools show up.

From the terminal — pipe a snippet to the running daemon:

echo "page_info()" | nekoro-browser
# → {"ok": true, "result": {"title": "...", "url": "..."}}

Something down? nekoro-browser --doctor checks daemon / extension / service worker and tells you which one.


Examples

Send a multi-step flow in one shot. Every helper is already await-able at top level — no asyncio boilerplate, no imports:

nekoro-browser <<'PY'
await new_tab("https://example.com")
print((await page_info())["title"])            # Example Domain
print((await get_markdown(max_chars=200))["result"])
print((await state(max_items=3))["result"])    # indexed interactive elements, model-ready
await close_tab()
PY
On Windows? <<'PY' is bash-only — PowerShell equivalent
@'
await new_tab("https://example.com")
print((await page_info())["title"])
'@ | nekoro-browser

The closing '@ must sit at the start of its own line. One-liners: nekoro-browser -c "await navigate('https://example.com')".

Not cmd.exe — its echo keeps the quotes, so the snippet arrives as a string and comes back {"ok": true, "result": "page_info()"} with the browser untouched.

state() numbers the elements and click_index(n) clicks by number — the model never has to guess a CSS selector:

nekoro-browser <<'PY'
await navigate("https://github.com/search?q=browser+automation&type=repositories")
await wait_for_load()
print((await state(max_items=40))["result"])   # every interactive element carries an index
PY

Then click the one you saw — indices shift with page content, so don't copy a fixed number:

nekoro-browser -c "await click_index(7)"

All helpers are documented in SKILL.md.


How It Compares

CDP WebSocket playwright-cli opencli nekoro-browser
Approach --remote-debugging-port Playwright ext. OpenCLI ext. Custom ext. + WS
Install one flag npm i -g (~200MB) npm / desktop uv tool install (stdlib only)
Login state ❌ fresh instance
Modify ext. edit source edit source ✅ this repo
Self-healing ✅ agent edits at runtime
MCP ✅ separate pkg ✅ built-in, 53 tools
Site knowledge ✅ notes auto-attached

Why row 3 is ❌: since Chrome 136, --remote-debugging-port refuses the default profile — a raw CDP connection means a fresh instance with none of your logins. An extension's chrome.debugger is exempt.


MCP (any MCP client)

MCP is how Claude Code, Cursor and friends call outside tools. Hook it up once and the model gets navigate, click_index, get_markdown… as first-class tools.

Prerequisite: the daemon is running (nekoro-browser, its own terminal) — the MCP server is a thin forwarder, the daemon owns the Chrome connection.

The command to register is always nekoro-browser-mcp. Only the config shape differs:

Claude Code

claude mcp add nekoro-browser -- nekoro-browser-mcp

Claude Desktop (Settings → Developer → Edit Config) · Cursor (~/.cursor/mcp.json, or .cursor/mcp.json for one project) · Cline (MCP Servers → Configure MCP Servers)

{ "mcpServers": { "nekoro-browser": { "command": "nekoro-browser-mcp" } } }

Claude Desktop config file: macOS ~/Library/Application Support/Claude/claude_desktop_config.json · Windows %APPDATA%\Claude\claude_desktop_config.json

opencode (opencode.json) — note command is an array, and the key is mcp

{ "mcp": { "nekoro-browser": { "type": "local", "command": ["nekoro-browser-mcp"], "enabled": true } } }

Codex (~/.codex/config.toml, or codex mcp add nekoro-browser -- nekoro-browser-mcp)

[mcp_servers.nekoro-browser]
command = "nekoro-browser-mcp"

VS Code / Copilot (.vscode/mcp.json, or MCP: Open User Configuration) — the key is servers, not mcpServers

{ "servers": { "nekoro-browser": { "command": "nekoro-browser-mcp" } } }

Prefer not to install anything up front? Replace the command with uvx, which fetches and runs on demand the way npx -y does — e.g. "command": "uvx", "args": ["--from", "nekoro-browser", "nekoro-browser-mcp"]. That only removes the install step for the MCP server; the daemon still has to be installed and running.

Restart the client afterwards. If the tools don't show up, run nekoro-browser --doctor first — a dead daemon looks exactly like a broken MCP config — then check the client's MCP log (Claude Desktop keeps them in ~/Library/Logs/Claude on macOS, %APPDATA%\Claude\logs on Windows).

Beyond the tool list:

  • cdp — raw CDP command, and exec_python — arbitrary Python in the daemon namespace, so a whole multi-step flow costs one round trip.
  • Screenshots return as image content; clients render them inline.
  • A helper failure ({"ok": false}) surfaces as isError, never dressed up as success.
  • Navigating to a site you have notes or scripts for ships them in the tool result — see Site Knowledge.

API

Category Commands
Navigation navigate(url), new_tab(url), ensure_tab(url), new_tab(url, reuse=True), list_tabs(), switch_tab(id), close_tab(id), close_tabs(ids), sweep_tabs()
Page info page_info(), page_html(), page_text(), get_markdown(), state(), refs(), find_text(t), iframe_target(url_substr)
JavaScript js(code), cdp(method, **p), cdp_batch(*cmds)
Interaction click(loc), click(loc, tab=id), click_selector(sel), click_ref(ref), click_index(n), click_at_xy(x,y), type_text(t), fill_input(sel,t), press_key(k), upload_file(sel,path)
Dialogs dialog_off(), get_last_dialog()
Waiting wait_for_load(), wait_selector(sel), wait_for_network_idle(), sleep(s)
Downloads wait_for_download()
Screenshots capture_screenshot(), capture_screenshot(scale="device"), capture_screenshot("jpeg", 90)

All page-level helpers take an optional tab= (default: the active tab). capture_screenshot defaults to scale="css" — pixel size equals the CSS viewport, so coordinates can be fed straight to click_at_xy; scale="device" keeps physical pixels.


Architecture

flowchart TD
    A["Chrome tab — your profile, your logins"]
    B["Extension background.js<br/>chrome.debugger / CDP"]
    C["Python daemon<br/>127.0.0.1:28417"]
    D["CLI<br/>nekoro-browser"]
    E["MCP server<br/>nekoro-browser-mcp"]

    A <-->|CDP| B
    B <-->|persistent WebSocket| C
    D -->|"HTTP /exec · token auth"| C
    E -->|"HTTP /exec · token auth"| C
Same diagram as plain text (for renderers without Mermaid, e.g. PyPI)
Chrome extension (background.js) —— chrome.debugger / CDP
        ↕ persistent WebSocket
Python daemon (127.0.0.1:28417)
        ↕ HTTP /exec (token auth)
CLI (nekoro-browser)  ·  MCP server (nekoro-browser-mcp)
  • helpers.py — 54 helpers (53 exposed as MCP tools), none aware of any particular website.
  • lifecycle.py — pid file + process fingerprint (never kills a reused pid), stale-daemon self-heal (CDP probe fails → cleanup and restart), localhost bypasses the system proxy.
  • Extension, against MV3 service worker eviction — content_scripts heartbeat (wake vector living in the page, revives a killed SW) + onStartup (reconnects on Chrome cold start) + reattaches the last-driven tab instead of drifting to a blank one.

Self-Healing and Site Knowledge

When an agent hits a gap it writes the missing piece and uses it immediately — nothing is recompiled, no daemon restart, no extension reload.

  • src/nekoro_browser/agent_helpers.py is scratch paper: reloaded on every /exec, good for a quick experiment. It lives inside the installed package, so an upgrade overwrites it.
  • Anything worth keeping goes in your own skills directory (NEKORO_DOMAIN_SKILLS, falling back to domain-skills/ in the repo), one folder per site holding both kinds of material: <site>/*.md for knowledge and <site>/*.py for workflows. Scripts are loaded into the /exec namespace on every call and can use the built-in helpers directly.

The point is that this material finds the agent instead of waiting to be discovered. navigate() and new_tab() return two extra fields when the site has any:

{'ok': True, 'loaded': True,
 'notes':   ['example/search.md — Example — search results'],
 'actions': ['open_first_result(query) — search and open the top hit']}

notes lists titles only; actions lists functions that are already callable, so the agent runs one instead of rebuilding the flow. list_site_actions() shows everything loaded, failed files included. What to record — and what not to — is in domain-skills/README.md.

Tabs work the same way: a tab left over from last time still holds its login and page state, so new_tab() adds an existing field when the managed group already has tabs for that site:

{'ok': True, 'tabId': 42, 'loaded': True,
 'existing': {'hint': 'switch_tab(id) reuses an open tab, or new_tab(url, reuse=True)',
              'tabs': [{'tabId': 17, 'title': 'Example Domain'}]}}

The tab still opens — the field only makes reuse visible at the moment a duplicate is about to appear; reuse=True navigates the existing one instead. Nothing is ever closed automatically: sweep_tabs() only reports candidates (same-site duplicates, stray about:blank), sweep_tabs(dry_run=False) / close_tabs([...]) act on them, and the active tab is never a candidate.


Platform Support

Platform Status
Windows Primary development platform, exercised end to end
Linux / macOS Platform branches + CI, full Chrome loop untested — reports welcome

Linux/macOS have the platform branches (~/.config / ~/Library/Application Support data dirs, chmod 600 token, /proc + ps liveness probes) and CI runs unit tests on all three — but the full "Chrome + extension" loop has never run on a real macOS/Linux box.

Known Limitations

  • Unpacked extensions get disabled by Chrome. An extension installed via "Load unpacked" may be switched off automatically after a Chrome update or restart, or hidden behind the "Disable developer mode extensions" prompt. When --doctor reports Extension/SW not responding, re-enable it in chrome://extensions/ first. This project is not published to the Chrome Web Store, so the limitation is not going away soon.
  • Service worker keepalive is not 100%. MV3 eviction timing is Chrome's call. The heartbeat + onStartup + reattach cover the vast majority of cases, but unattended long-running cron jobs should still health-check with --doctor and retry.
  • Everything is anchored to one active tab. 16 helpers (click, click_selector, state, wait_selector, fill_input, …) take an explicit tab=id to target another already attached tab — naming a tab that is not attached is an error, never a silent fallback to the active one. The other 37 always follow the active tab, and there are still no parallel sessions: one daemon drives one Chrome, requests are serialised.
  • Downloads land wherever Chrome is configured to put them; the path cannot be changed from here. wait_for_download() returns {url, filename, bytes} — a filename, not a full path. Set the directory in Chrome's own settings. Both Browser.setDownloadBehavior (-32601) and the deprecated Page.setDownloadBehavior (-32000 "Cannot not access browser-level commands") are browser-level and get rejected under chrome.debugger, which only ever hands out a tab target.
  • The MCP server handles requests serially. During a wait_selector(timeout=90) every other request on that connection (including ping) queues behind it. Open separate client connections if you need concurrency.

Reference

CLI flags, configuration, troubleshooting, security — click to expand

CLI

Command What it does
nekoro-browser Start the daemon (foreground)
nekoro-browser setup Guided install: copies the extension path, then waits until the extension actually connects
nekoro-browser --ensure Self-healing readiness check: launches Chrome if it isn't running, starts the daemon in the background if it isn't up, reloads the service worker if it isn't answering. Exit 0 only when all green — run this before a task instead of doing the steps by hand. It never starts a second daemon on a port that is already held; it reports the pid and stops
nekoro-browser --doctor End-to-end diagnostic (daemon + extension + SW all alive?) — reports only, repairs nothing
nekoro-browser --stop Stop the daemon
nekoro-browser --restart Stop and restart (foreground)
nekoro-browser --reload-ext Reload the extension's service worker — required after upgrading, also useful before a batch job for a clean state
nekoro-browser --extension-path Print the extension directory (for "Load unpacked")
nekoro-browser --version Print the installed version (check it against the extension you loaded)
nekoro-browser --port N Run the daemon on port N (default 28417)
nekoro-browser -c "code" Run one snippet, print the result
nekoro-browser --timeout N Seconds to allow a snippet (default 120 — page loads are slow)
nekoro-browser --allow-domains "jd.com,*.taobao.com" Only allow these domains (comma-separated); unset = unrestricted
echo "code" | nekoro-browser Pipe mode (daemon must already be running)

Configuration

The daemon listens on 28417 by default. To change it:

Side How
Python (daemon + CLI + MCP) nekoro-browser --port 30500, or set NEKORO_PORT=30500
Extension Extension details → Extension options → set the port → Save (reconnects immediately, no reload)

Both sides must agree. Clients don't need the flag repeated: the daemon records its actual port in <data dir>/port, so a plain echo ... | nekoro-browser finds a daemon running on a non-default port. Precedence is --port > NEKORO_PORT > that file > default.

The data dir holding token / pid / port is %LOCALAPPDATA%\nekoro-browser on Windows, ~/Library/Application Support/nekoro-browser on macOS, $XDG_CONFIG_HOME/nekoro-browser or ~/.config/nekoro-browser elsewhere. NEKORO_DATA_DIR overrides it on any platform — it replaces the parent of that path; a nekoro-browser/ directory is still created inside it. So with NEKORO_DATA_DIR=/my/dir the token lives at /my/dir/nekoro-browser/token, not /my/dir/token.

The same limit can be set via NEKORO_ALLOW_DOMAINS (comma-separated, same syntax). Rule syntax: example.com matches exactly; *.example.com matches subdomains and the bare domain; * allows everything. See Security below.

Troubleshooting

Symptom Cause Fix
Daemon not running Daemon not started Run nekoro-browser in terminal 1
CDP timeout Extension not connected / service worker asleep nekoro-browser --doctor to diagnose; try --reload-ext or manually reload in chrome://extensions
Extension disabled by Chrome Unpacked extension + Chrome update Re-enable it in chrome://extensions/, then re-run --doctor
Page unchanged Extension not attached to tab Open a regular (non-chrome://) page, restart daemon
Another debugger is already attached Another debugging extension owns that tab (Playwright, OpenCLI, Claude in Chrome all use chrome.debugger) Only one debugger per tab. Use a different tab, or disable the other extension in chrome://extensions
Port in use Stale process Kill the process on port 28417, or just run nekoro-browser --stop
Red Errors badge on the extension card in chrome://extensions Daemon isn't running; the extension keeps retrying The extension is not broken. Start the daemon (nekoro-browser) — no new entries after that; clear the old ones with "Clear all" on the card

Nearly everyone hits the last one: between loading the extension and starting the daemon, every reconnect logs WebSocket connection to 'ws://127.0.0.1:28417/ws' failed: ERR_CONNECTION_REFUSED. Chrome's network stack emits that message below the JS layer — the extension's try/catch and ws.onerror cannot suppress it, and probing with fetch first logs the same thing. It can be explained, not silenced.

Security

The daemon listens on 127.0.0.1 and /exec runs arbitrary Python, so the transport is guarded:

  • CLI / MCP → daemon (/exec, /raw): a per-session token is written to a user-private file — %LOCALAPPDATA%\nekoro-browser\token on Windows, ~/Library/Application Support/nekoro-browser/token on macOS, $XDG_CONFIG_HOME or ~/.config/nekoro-browser/token elsewhere, chmod 600 on POSIX. Clients read it and send X-Nekoro-Token; missing/wrong token → 403. Web pages and remote hosts can't read local files, so they can't obtain it. /ping stays open.
  • Extension → daemon (/ws): the handshake Origin must be chrome-extension://…; a web page's WebSocket to localhost carries its own origin and is rejected.

Same-user local processes can read the token file — that boundary matches the OS user account, as with browser-harness's chmod 600.

  • Optional domain allowlist: --allow-domains "jd.com,*.taobao.com" (or NEKORO_ALLOW_DOMAINS) gates navigate / new_tab to listed hosts — anything else is refused before reaching CDP. Unset = unrestricted (fail-open): this tool drives your personal Chrome, so the default stays permissive.

Feedback

Hit a problem, or missing a helper you need? Open an issue. For bugs, include the output of nekoro-browser --doctor, your Chrome version and OS — saves a round trip.

PRs welcome. Run the tests first: for f in tests/test_*.py; do uv run python "$f"; done (CI runs them on all three platforms too).


Acknowledgments

Core architecture derived from:

  • browser-harness — thin-wrapper philosophy (each function is a CDP alias, ≤10 lines), pipe mode, self-healing agent_helpers.py, domain-skills directory structure, cdp() raw access
  • browser-actstate() indexed element tree, *[N] change markers, waitSelector() state polling, getMarkdown() page extraction
  • Playwright — CDP Input.dispatchMouseEvent real mouse events (isTrusted:true), extension + daemon dual-path architecture

Ideas drawn from:

  • ego-lite — "code base, not CLI base" (agent writes a script, not a command loop), unified locator syntax (css: / text: / xpath= …) with transient/permanent element-resolution errors as a retry/abandon signal (→ click()), "name says the intent" openOrReuseTab ergonomics (→ ensure_tab()), and experience-accumulation as a first-class design goal (nekoro's domain-skills already chase this)

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nekoro_browser-0.3.4.tar.gz (213.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nekoro_browser-0.3.4-py3-none-any.whl (134.8 kB view details)

Uploaded Python 3

File details

Details for the file nekoro_browser-0.3.4.tar.gz.

File metadata

  • Download URL: nekoro_browser-0.3.4.tar.gz
  • Upload date:
  • Size: 213.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for nekoro_browser-0.3.4.tar.gz
Algorithm Hash digest
SHA256 b5b9e121ac7cbcd041d41ac15e7422b0fdae653ada9a729d25b14f807002174b
MD5 9e8234cbc2f093dbd6de6aaaf40cc5ac
BLAKE2b-256 3350fbea00adc67fc25b626cd4fd02ed2cdde171354fd51cf2b14eb48d74bed0

See more details on using hashes here.

Provenance

The following attestation bundles were made for nekoro_browser-0.3.4.tar.gz:

Publisher: publish.yml on zeshuochen/nekoro-browser

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file nekoro_browser-0.3.4-py3-none-any.whl.

File metadata

  • Download URL: nekoro_browser-0.3.4-py3-none-any.whl
  • Upload date:
  • Size: 134.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for nekoro_browser-0.3.4-py3-none-any.whl
Algorithm Hash digest
SHA256 528e30ed81872930b268e8d2d3080b22680d73a1160ea092ac2e649e8c3f53df
MD5 ae2cae65e08e43b872bc3dfcce7cf1ab
BLAKE2b-256 9a74ece2b30b019634d549cc0b2e88da8e6596986560c47474f6c494af72bb27

See more details on using hashes here.

Provenance

The following attestation bundles were made for nekoro_browser-0.3.4-py3-none-any.whl:

Publisher: publish.yml on zeshuochen/nekoro-browser

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.3.5

2 files

This release

0.3.4 This release

2 files

0.3.3

2 files

0.3.2

2 files

0.3.1

2 files

0.3.0

2 files

0.2.3

2 files

0.2.2

2 files

0.2.1

2 files

0.2.0

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

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