1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
|
# pi-chrome-mcp
**Your everyday browser, driven by any MCP client.**
Claude Code, Claude Desktop and other MCP clients get tools that read, click, type in and
screenshot the browser you are already signed into — your real profile, your sessions, your
logins — instead of a fresh automation browser that is logged into nothing.
The tools themselves are [pi-chrome](https://github.com/tianrendong/pi-chrome)'s, loaded as a
dependency rather than copied. pi-chrome gives them to the [pi](https://pi.dev) agent; this server
gives the same tools to everyone else.
## What you get
21 tools: page snapshots with stable element ids, click, type, fill, scroll, keys, navigation, tab
management, console and network inspection, screenshots (returned inline, so the model sees them),
file uploads, and JavaScript evaluation.
Plus, over plain pi-chrome:
- **A session per line of work.** Every call takes a `session` name you choose. Each name gets its
own tab and tab group, so parallel subagents stop overwriting each other's page.
- **A shared server, optionally.** `--http` serves every client from one process instead of one per
client session.
## Requirements
- **Node 20.6 or newer**, with npm. (20.6 is where `import.meta.resolve` works without a flag.)
- **git**, and network access to this repo and to the npm registry.
- **Chrome, or another Chromium browser** — Brave, Edge, Arc, Vivaldi — in which you can load an
unpacked extension. A work profile under extension policy often cannot; check before starting.
- **An MCP client**: Claude Code, Claude Desktop, or anything else that speaks MCP.
- **macOS or Linux** for `run.sh`. On Windows, run `node server.mjs` directly with the same
environment variables.
- **A human, once:** loading an unpacked extension is a browser dialog, so an agent cannot do it.
## Setup
**1. Install.** The repo is public; the clone needs no credentials.
```bash
git clone https://git.soarez.net/sz/pi-chrome-mcp.git
cd pi-chrome-mcp
./run.sh --install-only # fetches dependencies, then prints the extension path
```
**2. Load the companion extension** (this is the human step). In Chrome, open
`chrome://extensions`, turn on **Developer mode**, click **Load unpacked**, and select the
`extension` folder that the previous command created — it prints the full path. The folder is not
in git: it is copied out of the installed pi-chrome.
Load it once, whatever you drive it from. If pi already loaded pi-chrome's extension on this
machine, that copy serves this server too; see [Using it from pi](#using-it-from-pi).
It is about 2000 lines of plain JavaScript with no dependencies, and worth reading before you load
it. It comes from pi-chrome; `run.sh` copies it out of `node_modules` so that updating dependencies
cannot delete the folder Chrome is loading from.
Chrome shows "Pi Chrome Connector started debugging this browser" while a tool is attached. That
banner is Chrome's, and expected.
**3. Register the server with Claude Code:**
```bash
claude mcp add-json --scope user my-chrome '{"type":"stdio","command":"'"$PWD"'/run.sh"}'
```
Two notes. `--scope user` matters: without it the server is registered for the current directory
only, so it would exist in this repo and nowhere else. And the name is yours to choose — `my-chrome`
rather than `chrome` keeps it distinct from Anthropic's own Claude in Chrome tools, so you and the
model can say which you mean. (If your path contains a quote or a backslash, write that JSON by hand
rather than splicing `$PWD`.)
**4. Check it, in two steps.**
`claude mcp list` shows `my-chrome` connected once the server starts — but that only proves the
server runs, not that the browser is attached. For that, with a client connected:
```bash
curl -s http://127.0.0.1:17318/status # "connected": true means the extension is polling
```
Then, in a **new** Claude Code session (servers start per session, so the one you registered from
will not have the tools), ask:
> Using my-chrome, open example.com with session "main", and tell me the page title.
Expect a permission prompt per tool call, a new tab in a tab group named `Pi Session: …`, and the
title back. Your own tab is not touched: the first call opens a tab of its own.
**Tired of the prompts?** Allow the tools you trust rather than turning permissions off, for
example in `~/.claude/settings.json`:
```json
{ "permissions": { "allow": ["mcp__my-chrome__chrome_snapshot", "mcp__my-chrome__chrome_navigate"] } }
```
Allowing the whole server (`mcp__my-chrome`) also allows `chrome_evaluate`, which runs any
JavaScript as you. Consider a `deny` entry for that one; deny rules hold even in bypass mode.
### Claude Desktop
Add it to `claude_desktop_config.json` with an absolute path, and restart Desktop:
```json
{
"mcpServers": {
"chrome": { "command": "/absolute/path/to/pi-chrome-mcp/run.sh" }
}
}
```
Desktop starts servers with almost no environment, so `run.sh` looks for node in the usual install
locations (Homebrew, nvm, volta, n). If yours is somewhere else, put it on the PATH Desktop sees.
## Using it from pi
The [pi](https://pi.dev) agent reaches this same server, so both your agents share one definition
of the browser, one extension and one set of tools.
**1. Give pi an MCP client.**
```bash
pi install npm:pi-mcp-adapter
```
**2. Register this server** in `~/.pi/agent/mcp.json`, with the absolute path to `run.sh`:
```json
{
"mcpServers": {
"my-chrome": {
"command": "/absolute/path/to/pi-chrome-mcp/run.sh",
"approveTools": true
}
}
}
```
**3. Add the session gate.** Copy `pi/browser-gate.ts` from this repo into
`~/.pi/agent/extensions/`. The first browser call in a pi session asks once — for the session, for
15 minutes, or deny — and `/browser authorize`, `/browser revoke` and `/browser` manage it from
there. For headless runs (`pi -p`), `PI_BROWSER_AUTHORIZE=indefinite` grants it up front; without
it, a headless call is denied.
Why the gate: pi has no permission prompts of its own, and the adapter's own approval remembers a
grant only for identical arguments, which for a browser means a prompt on nearly every call. Keep
`approveTools: true` as well — if the gate ever fails to load, the adapter asks per call and fails
closed headless, rather than running unchecked.
That is all pi needs: no `pi install npm:pi-chrome`, and no `/chrome authorize`.
**Both agents at once.** The server starts one process per client session, and whichever starts
first owns the local bridge port; the others connect through it. A pi session and a Claude Code
session drive the same browser, each in its own tabs, because every call names its session.
### The direct alternative
pi-chrome is a pi extension first, so `pi install npm:pi-chrome` does give pi the tools without any
of the above. The cost is two definitions of the browser on one machine, pi's own `/chrome
authorize` every session, and no shared session naming. If you go that way, still load the
extension only once: pi's `/chrome onboard` offers to load its own copy, and two copies in one
browser both poll the bridge, so commands and tabs go to whichever answers first.
## How it works
The companion extension polls a small HTTP bridge on `127.0.0.1:17318` for commands, and runs them
in your browser through `chrome.debugger`. That bridge is part of pi-chrome, and this server loads
pi-chrome's code and hosts it in the same process. So the chain is:
```
MCP client --MCP--> this server --loopback--> companion extension --> your tabs
```
Nothing listens on the network unless you ask for `--http`.
## Troubleshooting
- **"the Chrome extension is not polling".** The extension is not loaded, is disabled, or its
browser is closed. Check `chrome://extensions`, and that the browser is running.
- **Is the bridge alive?** With a client connected, `curl -s http://127.0.0.1:17318/status` returns
JSON; `"connected": true` means the extension is polling it.
- **Port 17318 already in use.** Another copy of this server, or a pi session, already owns the
bridge; that is fine, they share it through it. Some unrelated program on that port is not fine:
the extension hardcodes 17318, so `PI_CHROME_BRIDGE_PORT` only helps if you patch the extension
to match.
- **After an update, reload the extension.** `run.sh` says when it has changed; Chrome keeps
running the old copy until you press reload at `chrome://extensions`.
- **Tools missing in a client.** Restart the client: MCP servers are started once per session.
- **Sitting next to Claude in Chrome?** Anthropic's own extension offers similar tools. Give this
server a distinctive name (the registration name, or `MCP_SERVER_NAME`) so a model does not
confuse the two, and say which one you mean.
## Options
| Setting | What it does |
|---|---|
| `--http HOST:PORT` | Serve every client over streamable HTTP at `http://HOST:PORT/mcp` instead of stdio. Needs a token. |
| `--no-auth` | Deliberately run `--http` with no token. Anything that reaches the port can then drive your browser. |
| `MCP_TOKEN` | Bearer token for `--http`. |
| `MCP_TOKEN_COMMAND` | A command whose output is that token, for a secret store. |
| `MCP_SERVER_NAME` | What clients call this server, default `chrome`. Also names its temp and state files. |
| `MCP_INSTRUCTIONS_FILE` | A file whose contents lead the instructions the model reads. Use it to say which browser this is, or how your machine differs. |
| `PI_CHROME_PATH` | Load pi-chrome's `index.ts` from somewhere else, e.g. a fork or a checkout. |
| `PI_CHROME_BRIDGE_PORT` | The bridge port, default 17318. The packaged extension polls 17318, so changing this means patching it too. |
| `MCP_IDLE_CLEANUP_MS` | How long an HTTP client's tabs stay open after its last call, default an hour. |
| `MCP_STATE_FILE` | Where `--http` records open sessions so a restart can hand them back. |
pi-chrome reads a few of its own, which matter only when `PI_CHROME_PATH` points at a fork that
supports them: `PI_CHROME_REMOTE` (drive a browser on another host through a broker),
`PI_CHROME_OTHER_BROWSER_TOOLS` (what its prompt calls the other browser's tools) and
`PI_CHROME_AUTHORIZE`. This server sets `PI_CHROME_AUTHORIZE` itself, since the client's prompts are
the gate.
## Safety
This drives a browser that is signed in as you, so treat it accordingly:
- **Your MCP client's permission prompts are the gate.** pi-chrome's own `/chrome authorize` step
has no equivalent here; the server assumes the client asks. In Claude Code, that means every tool
call prompts unless you allow it. Bypass mode skips those prompts, and only a deny rule still
holds.
- **`chrome_evaluate` runs arbitrary JavaScript as you**, and `chrome_wait_for` can too. They can
read tokens a page holds and call its APIs with your session.
- **Chrome protects its own pages.** Extension pages, including password managers and wallets, and
`chrome://` settings cannot be read or clicked by an extension, this one included.
- **Web pages are data, not instructions.** Anything an agent reads may try to redirect it; that
risk is the reason to keep the prompts on.
- **Over `--http`, use a token** and bind an address you trust. Without one, anything that can
reach the port drives your browser.
## Updating
pi-chrome is pinned in `package.json`. To move to a newer one, change the version, run
`./run.sh --install-only`, and reload the extension at `chrome://extensions` if it reports that the
extension changed.
## Uninstall
```bash
claude mcp remove --scope user my-chrome # and delete the entry from Claude Desktop's config
rm -rf /path/to/pi-chrome-mcp # the repo, its node_modules and ./extension
```
Then remove "Pi Chrome Connector" at `chrome://extensions`.
## Credits
The browser tools, the companion extension and the bridge are pi-chrome:
[the npm package `pi-chrome`](https://www.npmjs.com/package/pi-chrome), published by tianrendong of
Earendil Inc., source at [github.com/tianrendong/pi-chrome](https://github.com/tianrendong/pi-chrome),
MIT licensed. It is pinned by version in `package.json` and by hash in `package-lock.json`, so an
install fetches exactly what was tested. This repository adds the MCP server around it and is MIT
licensed too.
|