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 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.
@@ -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
+ [![CI](https://github.com/beSaif/zenpilot/actions/workflows/ci.yml/badge.svg)](https://github.com/beSaif/zenpilot/actions/workflows/ci.yml)
26
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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)
@@ -0,0 +1,226 @@
1
+ # zenpilot
2
+
3
+ [![CI](https://github.com/beSaif/zenpilot/actions/workflows/ci.yml/badge.svg)](https://github.com/beSaif/zenpilot/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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"]
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ __all__ = ["main"]