Lightweight browser automation CLI + MCP server. Drives your everyday Chrome through an extension — keeps your login state, no debug port, no banners.
中文
Quick Start · Examples · MCP · API · Architecture · Site Knowledge · Limitations · Reference
Quick Start
1 — Install (Python 3.12+, zero third-party dependencies)
uv tool install nekoro-browser
Installs both commands (nekoro-browser, nekoro-browser-mcp) into their own environment,
so nothing lands in your system Python. No uv? pipx install nekoro-browser works the same way.
From source: git clone https://github.com/zeshuochen/nekoro-browser && cd nekoro-browser && uv pip install -e .
Upgrading?
uv tool upgrade nekoro-browserupdates the Python side only. The extension Chrome already loaded keeps running the oldbackground.js, so anything that touches the extension silently stays on the old behaviour. Reload it after every upgrade:nekoro-browser --reload-ext(or Reload on the card inchrome://extensions). Runnekoro-browser --doctorafterwards to confirm the service worker answers.
2 — Load the extension
nekoro-browser setup
setup copies the extension directory to your clipboard and then waits — up to three
minutes — until the extension actually connects, so you find out it worked instead of
guessing. Meanwhile you do the part Chrome reserves for humans: open chrome://extensions/,
turn on Developer mode, click Load unpacked, paste the directory.
3 — Start the daemon — give it its own terminal and leave it open; it runs in the foreground and closing that window stops it
nekoro-browser
4 — Drive the browser from anywhere else
echo "page_info()" | nekoro-browser
# → {"ok": true, "result": {"title": "...", "url": "..."}}
That's it. If step 4 says the daemon isn't running, or a command times out, run
nekoro-browser --doctor — it checks the daemon, the extension and the service worker
separately and tells you which one is down.
Why Not --remote-debugging-port?
Since Chrome 136, --remote-debugging-port / --remote-debugging-pipe refuse the default profile — you must point Chrome at a non-default --user-data-dir, i.e. a clean instance with none of your logins. An extension's chrome.debugger is not subject to that restriction, which is why nekoro goes through an extension.
| CDP WebSocket | playwright-cli | opencli | nekoro-browser | |
|---|---|---|---|---|
| Approach | --remote-debugging-port |
Playwright extension | OpenCLI extension | Custom extension + persistent WebSocket |
| Install | one flag | npm i -g (~200MB) |
npm / desktop app | uv tool install (stdlib only, zero deps) |
| Login state | ❌ fresh instance | ✅ | ✅ | ✅ |
| Modify the extension | — | Edit Playwright source | Edit OpenCLI source | ✅ right in this repo |
| Self-healing | ❌ | ❌ | ❌ | ✅ Agent edits helpers at runtime |
| MCP | ❌ | ✅ (separate @playwright/mcp) |
❌ | ✅ built in, 53 tools via nekoro-browser-mcp |
| Site knowledge | ❌ | ❌ | ❌ | ✅ your notes and scripts are handed to the agent on navigate |
Examples
Send a multi-step flow in one shot with a heredoc. Every helper is a top-level await:
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
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"])
await click_index(12)
PY
All helpers are documented in SKILL.md.
MCP (any MCP client)
Every function in helpers.py is reflected into an MCP tool (46 today) — no glue code.
Prerequisite: the daemon must be running (nekoro-browser, its own terminal). The MCP
server is a thin forwarder — it talks to that daemon over the same authenticated path as
echo ... | nekoro-browser, and the daemon is what 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).
What you get beyond the tool list: two escape hatches ship as tools — cdp (raw CDP
command) and exec_python (arbitrary Python in the daemon namespace, so a whole multi-step
flow costs one round trip). Screenshots come back as image content so clients render them
inline. A helper's own failure ({"ok": false}) is surfaced as isError instead of being
dressed up as success. And when you navigate to a site you have notes or scripts for, they
ride along in the tool result — see Self-Healing and 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("jpeg", 90) |
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 thin wrappers) → CDP commands, none of them aware of any particular website.
lifecycle.py manages the daemon: pid file + process fingerprint (avoids killing a reused pid), self-heal on stale daemon (CDP probe fails → auto cleanup and restart), localhost requests bypass the system proxy.
The extension is hardened against MV3 service worker eviction: a content_scripts heartbeat (an independent wake vector living in the page, reconnects and wakes the SW even after it's killed) + onStartup (reconnects instantly on Chrome cold start) + reattaches the last-driven tab after a restart instead of drifting to a blank tab.
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.pyis 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 todomain-skills/in the repo), one folder per site holding both kinds of material:<site>/*.mdfor knowledge and<site>/*.pyfor workflows. Scripts are loaded into the/execnamespace 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 — full text on every navigation would turn a one-time write into a
permanent read cost. actions lists functions that are already callable, so the agent runs
one instead of rebuilding the flow. list_site_actions() shows everything loaded, including
files that failed to load. Conventions for what to record — and what not to — are in
domain-skills/README.md.
The same idea applies to tabs. A tab left over from last time is still the same tab — its
login and page state are intact — so new_tab() adds an existing field when the managed
group already holds 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 is still opened — the field only makes reuse visible at the moment a duplicate is
about to appear. Pass reuse=True to navigate an existing tab instead of opening one.
Nothing is ever closed automatically: whether a tab is clutter or an asset is the user's
call, so sweep_tabs() only reports candidates (same-site duplicates, stray
about:blank) and sweep_tabs(dry_run=False) / close_tabs([...]) act on them. The active
tab is never a candidate.
Platform Support
| Platform | Status |
|---|---|
| Windows | Primary development platform, exercised end to end |
| Linux / macOS | The code has the branches (XDG dirs, chmod 600 token, /proc and ps liveness probes) and CI runs the unit tests on all three, but the full "Chrome + extension" loop has never been run on a real macOS/Linux box — reports welcome |
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
--doctorreports Extension/SW not responding, re-enable it inchrome://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--doctorand retry. - Everything is anchored to one active tab. 16 helpers (
click,click_selector,state,wait_selector,fill_input, …) take an explicittab=idto 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, and the path cannot be changed from here.
wait_for_download()returns{url, filename, bytes}— a filename, not a full path. BothBrowser.setDownloadBehavior(-32601) and the deprecatedPage.setDownloadBehavior(-32000 "Cannot not access browser-level commands") are browser-level and get rejected underchrome.debugger's tab attach, which only ever hands out a tab target. Set the directory in Chrome's own settings. - The MCP server handles requests serially. During a
wait_selector(timeout=90)every other request on that connection (includingping) 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 --doctor |
End-to-end diagnostic (daemon + extension + SW all alive?) |
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) |
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.
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 |
| Port in use | Stale process | Kill the process on port 28417, or just run nekoro-browser --stop |
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,chmod 600on POSIX). Clients read it and sendX-Nekoro-Token; missing/wrong token →403. Web pages and remote hosts can't read local files, so they can't obtain it./pingstays open. - Extension → daemon (
/ws): the handshakeOriginmust bechrome-extension://…; a web page'sWebSocketto 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.
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-act —
state()indexed element tree,*[N]change markers,waitSelector()state polling,getMarkdown()page extraction - Playwright — CDP
Input.dispatchMouseEventreal 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=…) withtransient/permanentelement-resolution errors as a retry/abandon signal (→click()), "name says the intent"openOrReuseTabergonomics (→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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nekoro_browser-0.2.0.tar.gz.
File metadata
- Download URL: nekoro_browser-0.2.0.tar.gz
- Upload date:
- Size: 156.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f97673658e578082f9a0a4c6ff9383d255414237c69648782fe4dd85b7ae5c8a
|
|
| MD5 |
40f0bf96e38b97d7e7a7f97ad0252699
|
|
| BLAKE2b-256 |
5a690b2e2f31102fd8f661cdd51fe94b7bad1416968bcf23d0f3fb91d95e0d98
|
Provenance
The following attestation bundles were made for nekoro_browser-0.2.0.tar.gz:
Publisher:
publish.yml on zeshuochen/nekoro-browser
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nekoro_browser-0.2.0.tar.gz -
Subject digest:
f97673658e578082f9a0a4c6ff9383d255414237c69648782fe4dd85b7ae5c8a - Sigstore transparency entry: 2351538734
- Sigstore integration time:
-
Permalink:
zeshuochen/nekoro-browser@d7459653e410c1a036f60924d53016fffc0dfb19 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/zeshuochen
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d7459653e410c1a036f60924d53016fffc0dfb19 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file nekoro_browser-0.2.0-py3-none-any.whl.
File metadata
- Download URL: nekoro_browser-0.2.0-py3-none-any.whl
- Upload date:
- Size: 102.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
01a6d313cbcb4bd52148ad976b867c555159b018f9e7e15ddb447cd5fe6ce264
|
|
| MD5 |
5a9f9bbbffc97235c227a3491c194183
|
|
| BLAKE2b-256 |
fd63f0f7b83cef6f6ba71f89160b9681e44c202dcf1c4c79a564aa9d9586e7a9
|
Provenance
The following attestation bundles were made for nekoro_browser-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on zeshuochen/nekoro-browser
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nekoro_browser-0.2.0-py3-none-any.whl -
Subject digest:
01a6d313cbcb4bd52148ad976b867c555159b018f9e7e15ddb447cd5fe6ce264 - Sigstore transparency entry: 2351538775
- Sigstore integration time:
-
Permalink:
zeshuochen/nekoro-browser@d7459653e410c1a036f60924d53016fffc0dfb19 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/zeshuochen
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d7459653e410c1a036f60924d53016fffc0dfb19 -
Trigger Event:
workflow_dispatch
-
Statement type: