zenpilot 0.2.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.
- zenpilot-0.2.0/LICENSE +21 -0
- zenpilot-0.2.0/PKG-INFO +248 -0
- zenpilot-0.2.0/README.md +226 -0
- zenpilot-0.2.0/pyproject.toml +66 -0
- zenpilot-0.2.0/pyproject.toml.orig +52 -0
- zenpilot-0.2.0/src/zenpilot/__init__.py +3 -0
- zenpilot-0.2.0/src/zenpilot/bidi.py +109 -0
- zenpilot-0.2.0/src/zenpilot/browser.py +497 -0
- zenpilot-0.2.0/src/zenpilot/cli.py +231 -0
- zenpilot-0.2.0/src/zenpilot/dialogs.py +41 -0
- zenpilot-0.2.0/src/zenpilot/safety.py +157 -0
- zenpilot-0.2.0/src/zenpilot/server.py +308 -0
- zenpilot-0.2.0/src/zenpilot/tree.py +97 -0
- zenpilot-0.2.0/src/zenpilot/walker.js +173 -0
- zenpilot-0.2.0/src/zenpilot/zen.py +152 -0
zenpilot-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 beSaif
|
|
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.
|
zenpilot-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zenpilot
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: MCP server that lets Claude drive a running Zen Browser over WebDriver BiDi
|
|
5
|
+
Keywords: mcp,claude,zen-browser,firefox,webdriver-bidi,browser-automation
|
|
6
|
+
Author: beSaif
|
|
7
|
+
Author-email: beSaif <hishamkolikara@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Operating System :: MacOS
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
|
|
16
|
+
Requires-Dist: mcp>=2.3,<3
|
|
17
|
+
Requires-Dist: websockets>=15,<18
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Project-URL: Homepage, https://github.com/beSaif/zenpilot
|
|
20
|
+
Project-URL: Issues, https://github.com/beSaif/zenpilot/issues
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# zenpilot
|
|
24
|
+
|
|
25
|
+
[](https://github.com/beSaif/zenpilot/actions/workflows/ci.yml)
|
|
26
|
+
[](LICENSE)
|
|
27
|
+
|
|
28
|
+
An MCP server that lets Claude Desktop and Claude Code drive a running
|
|
29
|
+
[Zen Browser](https://zen-browser.app) the way Claude in Chrome drives Chrome.
|
|
30
|
+
|
|
31
|
+
It is one Python process speaking MCP over stdio. It talks to Zen over
|
|
32
|
+
[WebDriver BiDi](https://w3c.github.io/webdriver-bidi/), which Firefox (and so Zen)
|
|
33
|
+
exposes when started with `--remote-debugging-port`. There is no browser extension,
|
|
34
|
+
HTTP server or native messaging host.
|
|
35
|
+
|
|
36
|
+
> **Status: alpha.** Tested on macOS with Zen 1.23.2b (Firefox 157). The Python code is
|
|
37
|
+
> platform-neutral, but the setup below is macOS-specific. Plain Firefox should work the
|
|
38
|
+
> same way, but it isn't tested yet.
|
|
39
|
+
|
|
40
|
+
## Setup
|
|
41
|
+
|
|
42
|
+
Requires macOS and Zen in `/Applications`.
|
|
43
|
+
|
|
44
|
+
**Claude Desktop:** download `zenpilot-<version>.mcpb` from the
|
|
45
|
+
[latest release](https://github.com/beSaif/zenpilot/releases/latest) and open it.
|
|
46
|
+
Claude Desktop installs it as an extension and provides Python itself. That's all; the
|
|
47
|
+
rest happens in dialogs the first time Claude uses it (below).
|
|
48
|
+
|
|
49
|
+
**Claude Code, or both:** install with [uv](https://docs.astral.sh/uv/) and run the setup
|
|
50
|
+
wizard, which asks which profile to use and which sites to allow:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
uv tool install zenpilot
|
|
54
|
+
zenpilot setup
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
It writes zenpilot's prefs to the profile's `user.js`, registers zenpilot with Claude
|
|
58
|
+
Code (user scope) and Claude Desktop if they're installed (restart Claude Desktop
|
|
59
|
+
afterwards), then starts Zen and prints its tabs.
|
|
60
|
+
|
|
61
|
+
### First use
|
|
62
|
+
|
|
63
|
+
Nothing needs starting by hand. When Claude calls a tool:
|
|
64
|
+
|
|
65
|
+
- **No profile chosen yet** (the Desktop extension): a dialog asks which Zen profile
|
|
66
|
+
Claude should use. `zenpilot` is a separate profile, logged out of everything, that
|
|
67
|
+
zenpilot creates; your own profile lets Claude use sites you're logged into.
|
|
68
|
+
- **Zen isn't running:** zenpilot starts it on that profile with remote debugging. The
|
|
69
|
+
`zenpilot` profile runs as a second instance alongside your normal Zen.
|
|
70
|
+
- **The profile is already open without remote debugging** (the usual case for your own
|
|
71
|
+
profile): a dialog asks to quit and reopen Zen. Zen restores your tabs, but lazily,
|
|
72
|
+
so Claude only sees a restored tab once it has been opened.
|
|
73
|
+
- **A site isn't allowlisted:** a dialog asks *Don't Allow*, *Allow This Session* or
|
|
74
|
+
*Always Allow*. *Always* saves it to the allowlist. A declined site isn't asked about
|
|
75
|
+
again until the server restarts.
|
|
76
|
+
|
|
77
|
+
The dialogs are native macOS dialogs, so they work in every client and only the person
|
|
78
|
+
at the Mac can answer them; the model never sees them. They default to the safe answer,
|
|
79
|
+
so a stray Enter declines. Set `prompts = false` in the config to turn them all off.
|
|
80
|
+
|
|
81
|
+
Before Zen starts with remote debugging, zenpilot always sets
|
|
82
|
+
`remote.prefs.recommended = false` in the profile's `user.js`. Without it, Firefox's
|
|
83
|
+
remote agent rewrites about 40 prefs for test automation and Zen writes them into the
|
|
84
|
+
profile: Safe Browsing (phishing and malware), password saving and autofill, the popup
|
|
85
|
+
blocker, tracking protection, and crash session restore. If Zen doesn't quit cleanly,
|
|
86
|
+
they stay that way. zenpilot also only ever creates the `zenpilot` profile, so a
|
|
87
|
+
mistyped profile name fails with the list of real ones instead of making a new profile.
|
|
88
|
+
|
|
89
|
+
### Commands
|
|
90
|
+
|
|
91
|
+
| Command | What it does |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `zenpilot` | The MCP server. Claude runs this; you don't. |
|
|
94
|
+
| `zenpilot setup [--profile NAME]` | The wizard above; `--profile` skips the questions. `--no-register` and `--no-launch` skip registering and starting Zen. |
|
|
95
|
+
| `zenpilot allow [DOMAIN...]` | Add sites to the allowlist; `--remove` takes them off; no arguments prints it |
|
|
96
|
+
| `zenpilot launch [PROFILE]` | Start Zen with remote debugging, offering to reopen it if it's open without |
|
|
97
|
+
| `zenpilot doctor` | Check each part of the setup and print a fix for anything wrong |
|
|
98
|
+
|
|
99
|
+
Without installing, `uvx zenpilot setup` works too and registers `uvx zenpilot` as the
|
|
100
|
+
server command; then run the other commands as `uvx zenpilot ...`.
|
|
101
|
+
|
|
102
|
+
To hack on it: `git clone https://github.com/beSaif/zenpilot.git && cd zenpilot && uv sync
|
|
103
|
+
&& uv run zenpilot setup`. That registers the clone's `.venv/bin/zenpilot`, so edits take
|
|
104
|
+
effect the next time Claude starts the server.
|
|
105
|
+
|
|
106
|
+
## Tools
|
|
107
|
+
|
|
108
|
+
| Tool | What it does |
|
|
109
|
+
|---|---|
|
|
110
|
+
| `list_tabs` | Tabs with short ids (`t3`); `*` marks the active one |
|
|
111
|
+
| `new_tab(url?)` | Open a tab, make it active, optionally load `url` |
|
|
112
|
+
| `switch_tab(tab_id)` | Activate a tab |
|
|
113
|
+
| `close_tab(tab_id)` | Close a tab |
|
|
114
|
+
| `navigate(url, tab_id?)` | Load a URL and wait for the load event |
|
|
115
|
+
| `read_page(tab_id?)` | Indented outline of visible interactive elements and text, with refs like `r12` |
|
|
116
|
+
| `click(ref)` | Real mouse click on a ref |
|
|
117
|
+
| `type(ref, text, submit=false)` | Replace a field's content with real key events; on a `<select>` it picks the option starting with `text` |
|
|
118
|
+
| `scroll(direction, amount=600, tab_id?)` | Mouse-wheel scroll |
|
|
119
|
+
| `screenshot(tab_id?)` | JPEG of the viewport |
|
|
120
|
+
| `get_text(tab_id?, max_chars=8000)` | Readable text of `<main>`, a single `<article>`, or `<body>` |
|
|
121
|
+
|
|
122
|
+
`tab_id` defaults to the active tab. Input goes through `input.performActions`, so pages
|
|
123
|
+
see trusted events (`event.isTrusted === true`).
|
|
124
|
+
|
|
125
|
+
Example `read_page` output:
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
form
|
|
129
|
+
r3 textbox "Name" value="old value"
|
|
130
|
+
r5 textbox "Password" type=password value="•••"
|
|
131
|
+
r6 select "Color" value="Red"
|
|
132
|
+
r7 checkbox "I agree"
|
|
133
|
+
r9 button "Sign up"
|
|
134
|
+
frame "Same-origin frame"
|
|
135
|
+
r14 button "Frame button"
|
|
136
|
+
frame "Cross-origin frame" [skipped: 127.0.0.1 not allowlisted]
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Refs point to BiDi `sharedId` node references, not CSS selectors. They reach into open
|
|
140
|
+
shadow roots and into iframes, which are read through their own browsing context.
|
|
141
|
+
Calling `read_page` again, or navigating the page, invalidates a tab's refs. A stale or
|
|
142
|
+
unknown ref returns an error that tells the model to call `read_page` again. Output is
|
|
143
|
+
capped at about 12k characters; when a page is longer, plain text is dropped before
|
|
144
|
+
interactive elements.
|
|
145
|
+
|
|
146
|
+
## Safety
|
|
147
|
+
|
|
148
|
+
Config lives in `~/.config/zenpilot/config.toml` and is created on first run. Changes
|
|
149
|
+
apply on the next tool call. `zenpilot allow github.com` edits the allowlist for you.
|
|
150
|
+
|
|
151
|
+
```toml
|
|
152
|
+
allowlist = ["localhost", "127.0.0.1"] # "example.com" also covers its subdomains
|
|
153
|
+
read_only = false # true disables click, type, navigate, new_tab, close_tab
|
|
154
|
+
dangerously_allow_all_sites = false # true ignores the allowlist (see below)
|
|
155
|
+
profile = "zenpilot" # Zen profile to drive; asked on first use if unset
|
|
156
|
+
port = 9222
|
|
157
|
+
auto_launch = true # start Zen on that profile when a tool needs it
|
|
158
|
+
prompts = true # ask in macOS dialogs (see First use)
|
|
159
|
+
audit_log = "~/.local/state/zenpilot/audit.jsonl"
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
- **Allowlist.** Every tool that touches a page checks the tab's current domain. That
|
|
163
|
+
covers reading, screenshots, clicking and typing. `navigate` also checks both the
|
|
164
|
+
current domain and the target. Blocked calls return an error naming the domain and
|
|
165
|
+
the `zenpilot allow` command, so Claude can ask you to run it.
|
|
166
|
+
`list_tabs` hides the titles and URLs of other sites. A click can still take a tab to
|
|
167
|
+
another domain, but every call after that is blocked. Only `http(s)` and `about:blank`
|
|
168
|
+
are allowed; `file:`, `data:` and `about:config` are not.
|
|
169
|
+
- **Dangerous mode.** `dangerously_allow_all_sites = true` skips the allowlist for every
|
|
170
|
+
http(s) site. Non-web schemes stay blocked, and fencing, read-only mode and the audit
|
|
171
|
+
log still apply. Only the literal `true` turns it on. With a logged-in profile, this
|
|
172
|
+
means a prompt injection on any page you visit can steer Claude inside your accounts.
|
|
173
|
+
Prefer adding sites to the allowlist, or use it together with `read_only = true`.
|
|
174
|
+
- **Untrusted content.** Text from pages (`read_page`, `get_text`, tab titles) is wrapped
|
|
175
|
+
in `<untrusted_web_content>`, followed by a note that it is data, not instructions.
|
|
176
|
+
Fake closing tags inside the content are neutralized. Screenshots carry the same note.
|
|
177
|
+
- **Audit log.** Each call appends `{ts, tool, params, domain, ok, ms, error?}` as one
|
|
178
|
+
JSON line. Typed text is recorded only as a character count.
|
|
179
|
+
- **Not included, on purpose.** There are no tools for cookies, storage, clipboard,
|
|
180
|
+
downloads, passwords, clearing data, or running arbitrary JavaScript.
|
|
181
|
+
- **The debugging port.** Zen's remote agent listens only on 127.0.0.1. It rejects any
|
|
182
|
+
connection that sends an `Origin` header, so web pages can't connect to it. Don't pass
|
|
183
|
+
`--remote-allow-origins`. Any local process running as you can still connect when no
|
|
184
|
+
session is open, which is one more reason to use a dedicated profile.
|
|
185
|
+
|
|
186
|
+
## How it behaves
|
|
187
|
+
|
|
188
|
+
- **One session at a time.** Firefox allows one BiDi session. zenpilot opens it on the
|
|
189
|
+
first tool call and releases it after 2 minutes idle, so Claude Desktop and several
|
|
190
|
+
Claude Code sessions can take turns. Refs and tab ids expire when the session is
|
|
191
|
+
released. If another zenpilot holds the session, you get an "already has an active
|
|
192
|
+
automation session" error.
|
|
193
|
+
- **Stuck sessions.** Firefox keeps a session alive after its socket closes. zenpilot
|
|
194
|
+
ends the session on exit (stdin close, SIGTERM, SIGINT, SIGHUP). A `kill -9` can't be
|
|
195
|
+
handled, though, and after one Zen refuses new sessions until you restart it.
|
|
196
|
+
- **Covered window.** zenpilot works while Zen's window is hidden or covered. Firefox
|
|
197
|
+
doesn't answer `activate` in that state, or when Zen isn't the frontmost app, so
|
|
198
|
+
zenpilot gives up waiting after 1s and `new_tab` can take about a second. If wheel
|
|
199
|
+
scrolling doesn't move the page, it falls back to `scrollBy`.
|
|
200
|
+
- **Which tab is active.** BiDi has no "selected tab" API. zenpilot ranks tabs by
|
|
201
|
+
`document.hasFocus()` and visibility, and uses the tab it last opened, switched to or
|
|
202
|
+
navigated to as a tie-breaker.
|
|
203
|
+
- **No new windows.** Zen doesn't support creating windows over BiDi, so new tabs open
|
|
204
|
+
in the current window.
|
|
205
|
+
|
|
206
|
+
## Tests
|
|
207
|
+
|
|
208
|
+
```sh
|
|
209
|
+
uv run pytest -m "not integration" # unit tests: BiDi client (fake socket), ref tree, safety, CLI, prompts
|
|
210
|
+
uv run pytest # with Zen running (zenpilot launch): adds integration tests
|
|
211
|
+
uv run ruff check src tests && uv run ruff format src tests
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
CI runs the lint checks and unit tests on Python 3.12 and 3.13. Pushing a `v*` tag that
|
|
215
|
+
matches `pyproject.toml` runs `release.yml`: it publishes to PyPI (trusted publishing,
|
|
216
|
+
environment `pypi`) and creates a GitHub release with the wheel, sdist and `.mcpb`. The
|
|
217
|
+
extension's files are in `mcpb/`; a unit test keeps their version and tool list in sync.
|
|
218
|
+
|
|
219
|
+
The integration tests start the real `zenpilot` over MCP stdio and serve
|
|
220
|
+
`tests/fixture/` on localhost. The fixture has a form, buttons, a shadow-DOM widget,
|
|
221
|
+
same-origin and cross-origin iframes, and a link that opens a new tab. The tests
|
|
222
|
+
use a temporary config, so your allowlist and audit log aren't touched.
|
|
223
|
+
|
|
224
|
+
## Using your main profile
|
|
225
|
+
|
|
226
|
+
zenpilot works with any profile; the profile is only chosen when Zen starts. Pick yours
|
|
227
|
+
in the first-use dialog or in `zenpilot setup` (or set `profile` in the config, using
|
|
228
|
+
the name from `about:profiles`). Then, whenever Zen is open normally, Claude's next tool
|
|
229
|
+
call offers to reopen it with remote debugging.
|
|
230
|
+
|
|
231
|
+
Consider `auto_launch = false`, so that Claude can't start or reopen your logged-in
|
|
232
|
+
browser with the debugging port open. Start it with `zenpilot launch` when you want it.
|
|
233
|
+
|
|
234
|
+
While Zen runs this way:
|
|
235
|
+
|
|
236
|
+
- **Port access.** Any local process running as you can open a BiDi session on port
|
|
237
|
+
9222 whenever zenpilot isn't holding it. BiDi can read cookies and run script in your
|
|
238
|
+
logged-in sites, even though zenpilot exposes no such tools. Only run it like this
|
|
239
|
+
while you need it, and quit and relaunch Zen normally afterwards.
|
|
240
|
+
- **Automation visible to sites.** `navigator.webdriver` is `true` and Zen shows its
|
|
241
|
+
remote-control indicator. Some sites (banks, Cloudflare challenges) treat that as a
|
|
242
|
+
bot.
|
|
243
|
+
- **One debug instance.** Only one Zen instance can have remote debugging on port 9222
|
|
244
|
+
at a time.
|
|
245
|
+
|
|
246
|
+
## License
|
|
247
|
+
|
|
248
|
+
[MIT](LICENSE)
|
zenpilot-0.2.0/README.md
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# zenpilot
|
|
2
|
+
|
|
3
|
+
[](https://github.com/beSaif/zenpilot/actions/workflows/ci.yml)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
An MCP server that lets Claude Desktop and Claude Code drive a running
|
|
7
|
+
[Zen Browser](https://zen-browser.app) the way Claude in Chrome drives Chrome.
|
|
8
|
+
|
|
9
|
+
It is one Python process speaking MCP over stdio. It talks to Zen over
|
|
10
|
+
[WebDriver BiDi](https://w3c.github.io/webdriver-bidi/), which Firefox (and so Zen)
|
|
11
|
+
exposes when started with `--remote-debugging-port`. There is no browser extension,
|
|
12
|
+
HTTP server or native messaging host.
|
|
13
|
+
|
|
14
|
+
> **Status: alpha.** Tested on macOS with Zen 1.23.2b (Firefox 157). The Python code is
|
|
15
|
+
> platform-neutral, but the setup below is macOS-specific. Plain Firefox should work the
|
|
16
|
+
> same way, but it isn't tested yet.
|
|
17
|
+
|
|
18
|
+
## Setup
|
|
19
|
+
|
|
20
|
+
Requires macOS and Zen in `/Applications`.
|
|
21
|
+
|
|
22
|
+
**Claude Desktop:** download `zenpilot-<version>.mcpb` from the
|
|
23
|
+
[latest release](https://github.com/beSaif/zenpilot/releases/latest) and open it.
|
|
24
|
+
Claude Desktop installs it as an extension and provides Python itself. That's all; the
|
|
25
|
+
rest happens in dialogs the first time Claude uses it (below).
|
|
26
|
+
|
|
27
|
+
**Claude Code, or both:** install with [uv](https://docs.astral.sh/uv/) and run the setup
|
|
28
|
+
wizard, which asks which profile to use and which sites to allow:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
uv tool install zenpilot
|
|
32
|
+
zenpilot setup
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
It writes zenpilot's prefs to the profile's `user.js`, registers zenpilot with Claude
|
|
36
|
+
Code (user scope) and Claude Desktop if they're installed (restart Claude Desktop
|
|
37
|
+
afterwards), then starts Zen and prints its tabs.
|
|
38
|
+
|
|
39
|
+
### First use
|
|
40
|
+
|
|
41
|
+
Nothing needs starting by hand. When Claude calls a tool:
|
|
42
|
+
|
|
43
|
+
- **No profile chosen yet** (the Desktop extension): a dialog asks which Zen profile
|
|
44
|
+
Claude should use. `zenpilot` is a separate profile, logged out of everything, that
|
|
45
|
+
zenpilot creates; your own profile lets Claude use sites you're logged into.
|
|
46
|
+
- **Zen isn't running:** zenpilot starts it on that profile with remote debugging. The
|
|
47
|
+
`zenpilot` profile runs as a second instance alongside your normal Zen.
|
|
48
|
+
- **The profile is already open without remote debugging** (the usual case for your own
|
|
49
|
+
profile): a dialog asks to quit and reopen Zen. Zen restores your tabs, but lazily,
|
|
50
|
+
so Claude only sees a restored tab once it has been opened.
|
|
51
|
+
- **A site isn't allowlisted:** a dialog asks *Don't Allow*, *Allow This Session* or
|
|
52
|
+
*Always Allow*. *Always* saves it to the allowlist. A declined site isn't asked about
|
|
53
|
+
again until the server restarts.
|
|
54
|
+
|
|
55
|
+
The dialogs are native macOS dialogs, so they work in every client and only the person
|
|
56
|
+
at the Mac can answer them; the model never sees them. They default to the safe answer,
|
|
57
|
+
so a stray Enter declines. Set `prompts = false` in the config to turn them all off.
|
|
58
|
+
|
|
59
|
+
Before Zen starts with remote debugging, zenpilot always sets
|
|
60
|
+
`remote.prefs.recommended = false` in the profile's `user.js`. Without it, Firefox's
|
|
61
|
+
remote agent rewrites about 40 prefs for test automation and Zen writes them into the
|
|
62
|
+
profile: Safe Browsing (phishing and malware), password saving and autofill, the popup
|
|
63
|
+
blocker, tracking protection, and crash session restore. If Zen doesn't quit cleanly,
|
|
64
|
+
they stay that way. zenpilot also only ever creates the `zenpilot` profile, so a
|
|
65
|
+
mistyped profile name fails with the list of real ones instead of making a new profile.
|
|
66
|
+
|
|
67
|
+
### Commands
|
|
68
|
+
|
|
69
|
+
| Command | What it does |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `zenpilot` | The MCP server. Claude runs this; you don't. |
|
|
72
|
+
| `zenpilot setup [--profile NAME]` | The wizard above; `--profile` skips the questions. `--no-register` and `--no-launch` skip registering and starting Zen. |
|
|
73
|
+
| `zenpilot allow [DOMAIN...]` | Add sites to the allowlist; `--remove` takes them off; no arguments prints it |
|
|
74
|
+
| `zenpilot launch [PROFILE]` | Start Zen with remote debugging, offering to reopen it if it's open without |
|
|
75
|
+
| `zenpilot doctor` | Check each part of the setup and print a fix for anything wrong |
|
|
76
|
+
|
|
77
|
+
Without installing, `uvx zenpilot setup` works too and registers `uvx zenpilot` as the
|
|
78
|
+
server command; then run the other commands as `uvx zenpilot ...`.
|
|
79
|
+
|
|
80
|
+
To hack on it: `git clone https://github.com/beSaif/zenpilot.git && cd zenpilot && uv sync
|
|
81
|
+
&& uv run zenpilot setup`. That registers the clone's `.venv/bin/zenpilot`, so edits take
|
|
82
|
+
effect the next time Claude starts the server.
|
|
83
|
+
|
|
84
|
+
## Tools
|
|
85
|
+
|
|
86
|
+
| Tool | What it does |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `list_tabs` | Tabs with short ids (`t3`); `*` marks the active one |
|
|
89
|
+
| `new_tab(url?)` | Open a tab, make it active, optionally load `url` |
|
|
90
|
+
| `switch_tab(tab_id)` | Activate a tab |
|
|
91
|
+
| `close_tab(tab_id)` | Close a tab |
|
|
92
|
+
| `navigate(url, tab_id?)` | Load a URL and wait for the load event |
|
|
93
|
+
| `read_page(tab_id?)` | Indented outline of visible interactive elements and text, with refs like `r12` |
|
|
94
|
+
| `click(ref)` | Real mouse click on a ref |
|
|
95
|
+
| `type(ref, text, submit=false)` | Replace a field's content with real key events; on a `<select>` it picks the option starting with `text` |
|
|
96
|
+
| `scroll(direction, amount=600, tab_id?)` | Mouse-wheel scroll |
|
|
97
|
+
| `screenshot(tab_id?)` | JPEG of the viewport |
|
|
98
|
+
| `get_text(tab_id?, max_chars=8000)` | Readable text of `<main>`, a single `<article>`, or `<body>` |
|
|
99
|
+
|
|
100
|
+
`tab_id` defaults to the active tab. Input goes through `input.performActions`, so pages
|
|
101
|
+
see trusted events (`event.isTrusted === true`).
|
|
102
|
+
|
|
103
|
+
Example `read_page` output:
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
form
|
|
107
|
+
r3 textbox "Name" value="old value"
|
|
108
|
+
r5 textbox "Password" type=password value="•••"
|
|
109
|
+
r6 select "Color" value="Red"
|
|
110
|
+
r7 checkbox "I agree"
|
|
111
|
+
r9 button "Sign up"
|
|
112
|
+
frame "Same-origin frame"
|
|
113
|
+
r14 button "Frame button"
|
|
114
|
+
frame "Cross-origin frame" [skipped: 127.0.0.1 not allowlisted]
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Refs point to BiDi `sharedId` node references, not CSS selectors. They reach into open
|
|
118
|
+
shadow roots and into iframes, which are read through their own browsing context.
|
|
119
|
+
Calling `read_page` again, or navigating the page, invalidates a tab's refs. A stale or
|
|
120
|
+
unknown ref returns an error that tells the model to call `read_page` again. Output is
|
|
121
|
+
capped at about 12k characters; when a page is longer, plain text is dropped before
|
|
122
|
+
interactive elements.
|
|
123
|
+
|
|
124
|
+
## Safety
|
|
125
|
+
|
|
126
|
+
Config lives in `~/.config/zenpilot/config.toml` and is created on first run. Changes
|
|
127
|
+
apply on the next tool call. `zenpilot allow github.com` edits the allowlist for you.
|
|
128
|
+
|
|
129
|
+
```toml
|
|
130
|
+
allowlist = ["localhost", "127.0.0.1"] # "example.com" also covers its subdomains
|
|
131
|
+
read_only = false # true disables click, type, navigate, new_tab, close_tab
|
|
132
|
+
dangerously_allow_all_sites = false # true ignores the allowlist (see below)
|
|
133
|
+
profile = "zenpilot" # Zen profile to drive; asked on first use if unset
|
|
134
|
+
port = 9222
|
|
135
|
+
auto_launch = true # start Zen on that profile when a tool needs it
|
|
136
|
+
prompts = true # ask in macOS dialogs (see First use)
|
|
137
|
+
audit_log = "~/.local/state/zenpilot/audit.jsonl"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
- **Allowlist.** Every tool that touches a page checks the tab's current domain. That
|
|
141
|
+
covers reading, screenshots, clicking and typing. `navigate` also checks both the
|
|
142
|
+
current domain and the target. Blocked calls return an error naming the domain and
|
|
143
|
+
the `zenpilot allow` command, so Claude can ask you to run it.
|
|
144
|
+
`list_tabs` hides the titles and URLs of other sites. A click can still take a tab to
|
|
145
|
+
another domain, but every call after that is blocked. Only `http(s)` and `about:blank`
|
|
146
|
+
are allowed; `file:`, `data:` and `about:config` are not.
|
|
147
|
+
- **Dangerous mode.** `dangerously_allow_all_sites = true` skips the allowlist for every
|
|
148
|
+
http(s) site. Non-web schemes stay blocked, and fencing, read-only mode and the audit
|
|
149
|
+
log still apply. Only the literal `true` turns it on. With a logged-in profile, this
|
|
150
|
+
means a prompt injection on any page you visit can steer Claude inside your accounts.
|
|
151
|
+
Prefer adding sites to the allowlist, or use it together with `read_only = true`.
|
|
152
|
+
- **Untrusted content.** Text from pages (`read_page`, `get_text`, tab titles) is wrapped
|
|
153
|
+
in `<untrusted_web_content>`, followed by a note that it is data, not instructions.
|
|
154
|
+
Fake closing tags inside the content are neutralized. Screenshots carry the same note.
|
|
155
|
+
- **Audit log.** Each call appends `{ts, tool, params, domain, ok, ms, error?}` as one
|
|
156
|
+
JSON line. Typed text is recorded only as a character count.
|
|
157
|
+
- **Not included, on purpose.** There are no tools for cookies, storage, clipboard,
|
|
158
|
+
downloads, passwords, clearing data, or running arbitrary JavaScript.
|
|
159
|
+
- **The debugging port.** Zen's remote agent listens only on 127.0.0.1. It rejects any
|
|
160
|
+
connection that sends an `Origin` header, so web pages can't connect to it. Don't pass
|
|
161
|
+
`--remote-allow-origins`. Any local process running as you can still connect when no
|
|
162
|
+
session is open, which is one more reason to use a dedicated profile.
|
|
163
|
+
|
|
164
|
+
## How it behaves
|
|
165
|
+
|
|
166
|
+
- **One session at a time.** Firefox allows one BiDi session. zenpilot opens it on the
|
|
167
|
+
first tool call and releases it after 2 minutes idle, so Claude Desktop and several
|
|
168
|
+
Claude Code sessions can take turns. Refs and tab ids expire when the session is
|
|
169
|
+
released. If another zenpilot holds the session, you get an "already has an active
|
|
170
|
+
automation session" error.
|
|
171
|
+
- **Stuck sessions.** Firefox keeps a session alive after its socket closes. zenpilot
|
|
172
|
+
ends the session on exit (stdin close, SIGTERM, SIGINT, SIGHUP). A `kill -9` can't be
|
|
173
|
+
handled, though, and after one Zen refuses new sessions until you restart it.
|
|
174
|
+
- **Covered window.** zenpilot works while Zen's window is hidden or covered. Firefox
|
|
175
|
+
doesn't answer `activate` in that state, or when Zen isn't the frontmost app, so
|
|
176
|
+
zenpilot gives up waiting after 1s and `new_tab` can take about a second. If wheel
|
|
177
|
+
scrolling doesn't move the page, it falls back to `scrollBy`.
|
|
178
|
+
- **Which tab is active.** BiDi has no "selected tab" API. zenpilot ranks tabs by
|
|
179
|
+
`document.hasFocus()` and visibility, and uses the tab it last opened, switched to or
|
|
180
|
+
navigated to as a tie-breaker.
|
|
181
|
+
- **No new windows.** Zen doesn't support creating windows over BiDi, so new tabs open
|
|
182
|
+
in the current window.
|
|
183
|
+
|
|
184
|
+
## Tests
|
|
185
|
+
|
|
186
|
+
```sh
|
|
187
|
+
uv run pytest -m "not integration" # unit tests: BiDi client (fake socket), ref tree, safety, CLI, prompts
|
|
188
|
+
uv run pytest # with Zen running (zenpilot launch): adds integration tests
|
|
189
|
+
uv run ruff check src tests && uv run ruff format src tests
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
CI runs the lint checks and unit tests on Python 3.12 and 3.13. Pushing a `v*` tag that
|
|
193
|
+
matches `pyproject.toml` runs `release.yml`: it publishes to PyPI (trusted publishing,
|
|
194
|
+
environment `pypi`) and creates a GitHub release with the wheel, sdist and `.mcpb`. The
|
|
195
|
+
extension's files are in `mcpb/`; a unit test keeps their version and tool list in sync.
|
|
196
|
+
|
|
197
|
+
The integration tests start the real `zenpilot` over MCP stdio and serve
|
|
198
|
+
`tests/fixture/` on localhost. The fixture has a form, buttons, a shadow-DOM widget,
|
|
199
|
+
same-origin and cross-origin iframes, and a link that opens a new tab. The tests
|
|
200
|
+
use a temporary config, so your allowlist and audit log aren't touched.
|
|
201
|
+
|
|
202
|
+
## Using your main profile
|
|
203
|
+
|
|
204
|
+
zenpilot works with any profile; the profile is only chosen when Zen starts. Pick yours
|
|
205
|
+
in the first-use dialog or in `zenpilot setup` (or set `profile` in the config, using
|
|
206
|
+
the name from `about:profiles`). Then, whenever Zen is open normally, Claude's next tool
|
|
207
|
+
call offers to reopen it with remote debugging.
|
|
208
|
+
|
|
209
|
+
Consider `auto_launch = false`, so that Claude can't start or reopen your logged-in
|
|
210
|
+
browser with the debugging port open. Start it with `zenpilot launch` when you want it.
|
|
211
|
+
|
|
212
|
+
While Zen runs this way:
|
|
213
|
+
|
|
214
|
+
- **Port access.** Any local process running as you can open a BiDi session on port
|
|
215
|
+
9222 whenever zenpilot isn't holding it. BiDi can read cookies and run script in your
|
|
216
|
+
logged-in sites, even though zenpilot exposes no such tools. Only run it like this
|
|
217
|
+
while you need it, and quit and relaunch Zen normally afterwards.
|
|
218
|
+
- **Automation visible to sites.** `navigator.webdriver` is `true` and Zen shows its
|
|
219
|
+
remote-control indicator. Some sites (banks, Cloudflare challenges) treat that as a
|
|
220
|
+
bot.
|
|
221
|
+
- **One debug instance.** Only one Zen instance can have remote debugging on port 9222
|
|
222
|
+
at a time.
|
|
223
|
+
|
|
224
|
+
## License
|
|
225
|
+
|
|
226
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "zenpilot"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "MCP server that lets Claude drive a running Zen Browser over WebDriver BiDi"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
keywords = [
|
|
9
|
+
"mcp",
|
|
10
|
+
"claude",
|
|
11
|
+
"zen-browser",
|
|
12
|
+
"firefox",
|
|
13
|
+
"webdriver-bidi",
|
|
14
|
+
"browser-automation",
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Operating System :: MacOS",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Topic :: Internet :: WWW/HTTP :: Browsers",
|
|
23
|
+
]
|
|
24
|
+
requires-python = ">=3.12"
|
|
25
|
+
dependencies = [
|
|
26
|
+
"mcp>=2.3,<3",
|
|
27
|
+
"websockets>=15,<18",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[[project.authors]]
|
|
31
|
+
name = "beSaif"
|
|
32
|
+
email = "hishamkolikara@gmail.com"
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/beSaif/zenpilot"
|
|
36
|
+
Issues = "https://github.com/beSaif/zenpilot/issues"
|
|
37
|
+
|
|
38
|
+
[project.scripts]
|
|
39
|
+
zenpilot = "zenpilot:main"
|
|
40
|
+
|
|
41
|
+
[build-system]
|
|
42
|
+
requires = ["uv_build>=0.12.15,<0.13.0"]
|
|
43
|
+
build-backend = "uv_build"
|
|
44
|
+
|
|
45
|
+
[dependency-groups]
|
|
46
|
+
dev = [
|
|
47
|
+
"pytest>=9.1.1",
|
|
48
|
+
"pytest-asyncio>=1.4.0",
|
|
49
|
+
"ruff>=0.14",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[tool.ruff]
|
|
53
|
+
line-length = 120
|
|
54
|
+
|
|
55
|
+
[tool.ruff.lint]
|
|
56
|
+
select = [
|
|
57
|
+
"E",
|
|
58
|
+
"F",
|
|
59
|
+
"I",
|
|
60
|
+
"B",
|
|
61
|
+
"UP",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
[tool.pytest.ini_options]
|
|
65
|
+
asyncio_mode = "auto"
|
|
66
|
+
markers = ["integration: needs a live Zen on 127.0.0.1:9222"]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "zenpilot"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "MCP server that lets Claude drive a running Zen Browser over WebDriver BiDi"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
keywords = ["mcp", "claude", "zen-browser", "firefox", "webdriver-bidi", "browser-automation"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 3 - Alpha",
|
|
11
|
+
"Environment :: Console",
|
|
12
|
+
"Operating System :: MacOS",
|
|
13
|
+
"Programming Language :: Python :: 3.12",
|
|
14
|
+
"Programming Language :: Python :: 3.13",
|
|
15
|
+
"Topic :: Internet :: WWW/HTTP :: Browsers",
|
|
16
|
+
]
|
|
17
|
+
authors = [
|
|
18
|
+
{ name = "beSaif", email = "hishamkolikara@gmail.com" }
|
|
19
|
+
]
|
|
20
|
+
requires-python = ">=3.12"
|
|
21
|
+
dependencies = [
|
|
22
|
+
"mcp>=2.3,<3",
|
|
23
|
+
"websockets>=15,<18",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/beSaif/zenpilot"
|
|
28
|
+
Issues = "https://github.com/beSaif/zenpilot/issues"
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
zenpilot = "zenpilot:main"
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["uv_build>=0.12.15,<0.13.0"]
|
|
35
|
+
build-backend = "uv_build"
|
|
36
|
+
|
|
37
|
+
[dependency-groups]
|
|
38
|
+
dev = [
|
|
39
|
+
"pytest>=9.1.1",
|
|
40
|
+
"pytest-asyncio>=1.4.0",
|
|
41
|
+
"ruff>=0.14",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[tool.ruff]
|
|
45
|
+
line-length = 120
|
|
46
|
+
|
|
47
|
+
[tool.ruff.lint]
|
|
48
|
+
select = ["E", "F", "I", "B", "UP"]
|
|
49
|
+
|
|
50
|
+
[tool.pytest.ini_options]
|
|
51
|
+
asyncio_mode = "auto"
|
|
52
|
+
markers = ["integration: needs a live Zen on 127.0.0.1:9222"]
|