mcp-browser-extension 0.9.30 → 0.9.32
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.
- package/README.md +77 -14
- package/SETUP.md +40 -7
- package/dist/shared/activity.d.ts +25 -0
- package/dist/shared/activity.js +72 -0
- package/dist/shared/browser-permissions.d.ts +20 -0
- package/dist/shared/browser-permissions.js +40 -0
- package/dist/shared/client-auth.d.ts +28 -0
- package/dist/shared/client-auth.js +38 -0
- package/dist/shared/observers.d.ts +1 -1
- package/dist/shared/observers.js +1 -3
- package/dist/shared/policy.d.ts +5 -7
- package/dist/shared/policy.js +11 -26
- package/dist/shared/protocol.d.ts +4 -0
- package/dist/shared/protocol.js +3 -3
- package/dist/shared/screenshot.d.ts +4 -2
- package/dist/shared/screenshot.js +13 -8
- package/dist/src/access-browser.d.ts +15 -0
- package/dist/src/access-browser.js +202 -0
- package/dist/src/access.d.ts +2 -0
- package/dist/src/access.js +92 -18
- package/dist/src/bridge/client-identity.d.ts +10 -0
- package/dist/src/bridge/client-identity.js +28 -0
- package/dist/src/bridge/connection.d.ts +5 -0
- package/dist/src/bridge/connection.js +4 -12
- package/dist/src/bridge/peer.d.ts +4 -19
- package/dist/src/bridge/peer.js +17 -8
- package/dist/src/bridge/server.d.ts +7 -16
- package/dist/src/bridge/server.js +64 -17
- package/dist/src/bridge/site-grants.d.ts +1 -2
- package/dist/src/bridge/site-grants.js +4 -3
- package/dist/src/cli.js +1 -3
- package/dist/src/config.js +16 -5
- package/dist/src/executor/cdp-executor.d.ts +3 -0
- package/dist/src/executor/cdp-executor.js +69 -25
- package/dist/src/mcp/server.js +5 -3
- package/dist/src/mcp/tools.d.ts +0 -12
- package/dist/src/mcp/tools.js +48 -22
- package/dist/src/remote/key.js +12 -2
- package/dist/src/security/policy.d.ts +4 -0
- package/dist/src/security/policy.js +3 -1
- package/docs/BLUEPRINT.md +6 -1
- package/docs/CLIENT-APPROVAL.md +150 -0
- package/extension-dist/activity-dashboard.css +43 -0
- package/extension-dist/background.js +1036 -73
- package/extension-dist/manifest.json +10 -9
- package/extension-dist/options.html +101 -32
- package/extension-dist/options.js +558 -70
- package/extension-dist/popup.html +164 -90
- package/extension-dist/popup.js +576 -79
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -63,6 +63,12 @@ for one), so the question is what you get on top of the session:
|
|
|
63
63
|
keys and JWTs out of page reads.
|
|
64
64
|
- **An audit trail**: every call is logged with the URL, the allow/deny
|
|
65
65
|
verdict, duration and bytes returned.
|
|
66
|
+
- **Recent activity in Chrome**: the popup shows the latest five activity rows (a tab's open/close pair shares one row);
|
|
67
|
+
Settings shows up to 100 with outcome filters. This session history contains
|
|
68
|
+
website names and app keys, without page text, entered values or full URLs.
|
|
69
|
+
- **Stop access**: disconnect all apps from the popup or Settings. The stop
|
|
70
|
+
survives restarts; Resume connection requires fresh approval for each app.
|
|
71
|
+
A command already running may finish, and completed actions cannot be undone.
|
|
66
72
|
- **It stays out of your tab**: the agent opens its own tabs in the background
|
|
67
73
|
and works there, so the page you are on is never navigated, clicked or pulled
|
|
68
74
|
out of focus. `batch` reads many of them at once in one call.
|
|
@@ -138,14 +144,15 @@ with `/mcp` inside a session — no restart needed.
|
|
|
138
144
|
|
|
139
145
|
By default everything is **deny-all** (no domains, no eval, no mutations). Grant
|
|
140
146
|
exactly what you need with `--allow-domain <glob>` (repeatable), `--enable-mutations`,
|
|
141
|
-
`--enable-downloads`, `--enable-uploads`, `--unsafe-enable-eval
|
|
147
|
+
`--enable-downloads`, `--enable-uploads`, or `--unsafe-enable-eval`. All-site access
|
|
148
|
+
is not supported; approve named websites in the Chrome extension popup.
|
|
142
149
|
|
|
143
150
|
Email verification is on by default. For a trusted, local unattended setup, you
|
|
144
151
|
can opt out with `--no-email-verification` (or set
|
|
145
152
|
`MCP_BROWSER_EXTENSION_EMAIL_VERIFICATION=0`). This skips the email prompt and
|
|
146
153
|
the Firebase access service, so no email or device grant is sent there. The
|
|
147
|
-
local MCP client
|
|
148
|
-
|
|
154
|
+
local MCP client still needs approval inside Chrome before using the extension,
|
|
155
|
+
and stays within the configured domain and capability policy. Treat every process that can launch
|
|
149
156
|
or reach this local server as trusted. This option is rejected with `--tunnel`
|
|
150
157
|
and does not turn off telemetry or the npm update check.
|
|
151
158
|
|
|
@@ -155,7 +162,7 @@ and does not turn off telemetry or the npm update check.
|
|
|
155
162
|
> with `--uploads-dir <path>` to restrict uploads to files inside that directory
|
|
156
163
|
> (`..` traversal is blocked) — strongly recommended for unattended use.
|
|
157
164
|
|
|
158
|
-
**
|
|
165
|
+
**Keep transport pairing across restarts.** Both examples above include `--persist-token`, which
|
|
159
166
|
is what makes the pairing survive a restart — drop it if you'd rather have the
|
|
160
167
|
stricter default described next.
|
|
161
168
|
|
|
@@ -163,7 +170,8 @@ Without `--persist-token` a fresh token is minted every boot (the secure
|
|
|
163
170
|
default), which means re-pairing the extension on each restart. With it, the
|
|
164
171
|
token is stored 0600 at `~/.mcp-browser-extension/token` and reused; the extension's
|
|
165
172
|
keepalive auto-reconnects with no manual step. `MCP_BROWSER_EXTENSION_TOKEN` pins the token
|
|
166
|
-
explicitly (and is never written to disk).
|
|
173
|
+
explicitly (and is never written to disk). This transport pairing does not grant
|
|
174
|
+
an app access: every new app session has a separate key and needs Chrome approval.
|
|
167
175
|
|
|
168
176
|
**2. Install the extension** — **required**; the server can drive nothing without it.
|
|
169
177
|
|
|
@@ -174,13 +182,24 @@ the way to install it.
|
|
|
174
182
|
**3. Verify your email.** In your MCP client, ask the agent to call
|
|
175
183
|
`chrome_status`. On this first browser tool call, the client shows an email
|
|
176
184
|
prompt. Enter an address you can access, then open the sign-in link sent to
|
|
177
|
-
that address
|
|
185
|
+
that address and click **Verify this device**. The waiting tool reports
|
|
186
|
+
**Email verified** and continues.
|
|
178
187
|
|
|
179
188
|
If the tool returns `ACCESS_PENDING`, ask the agent to call `chrome_status`
|
|
180
189
|
again to keep waiting on the same link. You do not need another email or a
|
|
181
|
-
verification code.
|
|
182
|
-
|
|
183
|
-
|
|
190
|
+
verification code.
|
|
191
|
+
|
|
192
|
+
If your client suppresses the prompt (for example, in YOLO/bypass mode), does
|
|
193
|
+
not support MCP elicitation, or the dialog is dismissed, the server automatically
|
|
194
|
+
opens a temporary local verification form in your browser. The tool also returns
|
|
195
|
+
`ACCESS_INPUT_REQUIRED` with the link, so you can open it yourself if necessary
|
|
196
|
+
on the computer running the MCP server. Enter your email in the browser form,
|
|
197
|
+
and open the sign-in link from your inbox. Click **Verify this device**, then
|
|
198
|
+
call `chrome_status` to continue.
|
|
199
|
+
The local form expires after 10 minutes; call the tool again for a fresh one.
|
|
200
|
+
Opening the form does not send an email, and access remains blocked until the
|
|
201
|
+
email link and device grant are verified. Installing the extension alone does
|
|
202
|
+
not activate access.
|
|
184
203
|
|
|
185
204
|
**4. Click Connect if Chrome is not already paired.** A fresh install opens the
|
|
186
205
|
extension's Settings with a
|
|
@@ -191,6 +210,44 @@ token. Chrome lets only this extension talk to that helper. If the server ever
|
|
|
191
210
|
changes its token, the extension fetches the new one by itself. Nothing to copy
|
|
192
211
|
or paste.
|
|
193
212
|
|
|
213
|
+
**5. Approve the app connection in Chrome.** Run `chrome_status`, then open
|
|
214
|
+
the extension popup's **App connections** section. Compare the key
|
|
215
|
+
fingerprint with the one in the tool result and click **Approve for 8 hours**.
|
|
216
|
+
App names are supplied by the app; the key identifies this particular session.
|
|
217
|
+
Retry the tool after approving. A question-mark badge and the popup show pending
|
|
218
|
+
requests, even if the client hides its prompts. Approve, deny, revoke and restore
|
|
219
|
+
connections directly in the popup or in Settings.
|
|
220
|
+
|
|
221
|
+
Each app session has its own signing key, held only in that server session's memory.
|
|
222
|
+
Approval applies to this Chrome profile for eight hours. Restarting the app's MCP
|
|
223
|
+
connection creates a new key and needs a new approval. **Remove access** blocks further
|
|
224
|
+
commands from that key; an action already running may finish. YOLO, bypass-permission
|
|
225
|
+
mode and `--no-email-verification` cannot skip this extension check.
|
|
226
|
+
|
|
227
|
+
**6. Allow the websites you need.** The popup shows a Website access request.
|
|
228
|
+
Click **Allow**, then accept Chrome's website permission prompt if shown. This
|
|
229
|
+
single flow grants that named website in both Chrome and the server's allowlist.
|
|
230
|
+
Allowing a website lets approved apps read and take actions there, including
|
|
231
|
+
sending messages, deleting information and making purchases, subject to the
|
|
232
|
+
configured tool permissions. There is no separate extension approval for each
|
|
233
|
+
action. Website access is remembered until removed and shared by approved app
|
|
234
|
+
sessions; each app still needs its own unexpired connection approval.
|
|
235
|
+
Removing the website also removes its Chrome permission. Existing server flags
|
|
236
|
+
do not grant Chrome access, and an upgrade may ask you to approve websites again.
|
|
237
|
+
|
|
238
|
+
Cookies and downloads ask separately under **Extra permissions** when needed.
|
|
239
|
+
They start off, and their buttons also let you remove permission. Downloads are
|
|
240
|
+
fetched without browser cookies, redirects are refused, and files are limited to
|
|
241
|
+
50 MiB. Links that require a signed-in browser session or redirect may not
|
|
242
|
+
work. Proxy
|
|
243
|
+
authentication and IP lookups request permission from Settings. Chrome does not
|
|
244
|
+
support making `debugger` or `proxy` optional; those remain required, and debugger
|
|
245
|
+
commands still check website permission before they run.
|
|
246
|
+
|
|
247
|
+
Update both the extension and server before using client approval. An older
|
|
248
|
+
extension cannot enforce it, and the updated server refuses browser commands
|
|
249
|
+
until the extension is updated. See [client approval details](docs/CLIENT-APPROVAL.md).
|
|
250
|
+
|
|
194
251
|
<details>
|
|
195
252
|
<summary>Developers: load it unpacked instead</summary>
|
|
196
253
|
|
|
@@ -221,7 +278,8 @@ stays in the toolbar. Hover it for the status in words.
|
|
|
221
278
|
|
|
222
279
|
| Badge | Meaning |
|
|
223
280
|
|---|---|
|
|
224
|
-
| green dot | paired and connected |
|
|
281
|
+
| green dot | transport paired and connected |
|
|
282
|
+
| question mark | a connection or site needs your review |
|
|
225
283
|
| yellow dots | connecting |
|
|
226
284
|
| grey circle | not paired yet (no server has run, or no pairing file) |
|
|
227
285
|
| red exclamation mark | token rejected; the server rotated it, re-pairs by itself in a moment |
|
|
@@ -280,8 +338,9 @@ and the rest join the new hub. A call that was in flight at that moment fails
|
|
|
280
338
|
once with `EXTENSION_DISCONNECTED` and is retried automatically when it is safe
|
|
281
339
|
to repeat.
|
|
282
340
|
|
|
283
|
-
Peers authenticate with the pairing token from the 0600
|
|
284
|
-
|
|
341
|
+
Peers authenticate to the local transport with the pairing token from the 0600
|
|
342
|
+
handshake file. That token alone cannot authorize browser commands: each session
|
|
343
|
+
must sign its own commands with a key approved inside Chrome. A server too old to share the port is replaced as
|
|
285
344
|
before: it is verified to be an MCP Browser Extension server, then stopped. Anything that isn't a
|
|
286
345
|
verified MCP Browser Extension server is never touched — a port held by some other program is
|
|
287
346
|
reported, never killed.
|
|
@@ -654,7 +713,6 @@ status badge, and a stable pairing token (`--persist-token`).
|
|
|
654
713
|
```
|
|
655
714
|
mcp-browser-extension --allow-domain example.com --enable-mutations
|
|
656
715
|
mcp-browser-extension --policy ./policy.json # see policy.example.json
|
|
657
|
-
mcp-browser-extension --unsafe-all-domains # loud footgun
|
|
658
716
|
mcp-browser-extension --enable-observers # console/network/dialog capture (patches page globals)
|
|
659
717
|
mcp-browser-extension --redact # scrub secret-shaped strings out of page reads
|
|
660
718
|
```
|
|
@@ -666,7 +724,8 @@ is allowed (with its own Allow), and lists the allowed sites. The Options page
|
|
|
666
724
|
(Settings in the popup) also has a box to allow any other site. An allowed site applies at once to every session sharing that Chrome, and
|
|
667
725
|
is saved to `~/.mcp-browser-extension/allowed-sites.json` so it survives restarts. Sites
|
|
668
726
|
allowed there can be removed there; sites from `--allow-domain` stay until the
|
|
669
|
-
flags change.
|
|
727
|
+
flags change. Catch-all patterns do not grant access. Remove the retired
|
|
728
|
+
`--unsafe-all-domains` option from older configurations before starting. The
|
|
670
729
|
agent cannot approve a site itself: MCP Browser Extension never reads or drives the
|
|
671
730
|
extension's own pages. Run with `--no-site-grants` to keep the allowlist exactly
|
|
672
731
|
what the flags say.
|
|
@@ -675,6 +734,10 @@ what the flags say.
|
|
|
675
734
|
it says nothing about what is on them. A logged-in page routinely renders a
|
|
676
735
|
session token into a script tag or an API key onto a settings screen.
|
|
677
736
|
|
|
737
|
+
- **Cookie values are hidden by default.** `get_cookies` returns each cookie's
|
|
738
|
+
name, domain, path and flags with the value replaced by `[hidden]`. A session
|
|
739
|
+
cookie can grant access to an account without its password.
|
|
740
|
+
Pass `--enable-cookie-values` if a task really needs them.
|
|
678
741
|
- **Password field values are always suppressed** — in `get_html`, and in
|
|
679
742
|
`snapshot`, where the field still appears (so you can type into it) flagged
|
|
680
743
|
`secret: true` with no value. No flag, no opt-in: nobody wants those characters.
|
package/SETUP.md
CHANGED
|
@@ -14,8 +14,10 @@ inside Chrome's own UI (loading the extension, pasting the pairing token).
|
|
|
14
14
|
fix it. Do not proceed on a broken step.
|
|
15
15
|
- The server is **deny-all by default**: no domains, no clicks, no eval, no
|
|
16
16
|
downloads, no uploads. Ask the human which domains they want to automate and
|
|
17
|
-
grant only those with `--allow-domain
|
|
18
|
-
|
|
17
|
+
grant only those with `--allow-domain`, or let the person allow them from the
|
|
18
|
+
Chrome extension popup. All-site access is not supported. Remove
|
|
19
|
+
`--unsafe-all-domains` from older configurations; never replace it with a
|
|
20
|
+
catch-all pattern. Only enable `--unsafe-enable-eval` if the human asks by name.
|
|
19
21
|
- Never print or log the pairing token. It lives in a 0600 file; show the human
|
|
20
22
|
its path, not its contents.
|
|
21
23
|
- Requires Node 18 or newer (`node --version`) and Chrome 116 or newer.
|
|
@@ -128,15 +130,35 @@ the Web Store build passes review separately and can trail it by a version.
|
|
|
128
130
|
|
|
129
131
|
The first browser tool call opens an email prompt in the MCP client. Ask the
|
|
130
132
|
human to enter an email address. The website sends a Firebase sign-in link;
|
|
131
|
-
the human opens it in their browser
|
|
133
|
+
the human opens it in their browser, clicks **Verify this device**, and returns
|
|
134
|
+
to the MCP client. Do not ask
|
|
132
135
|
them to copy or paste a verification code. The server detects the verified
|
|
133
136
|
link automatically and reports the result to the waiting tool call. On success,
|
|
134
137
|
the result includes **Email verified** and the requested browser action continues.
|
|
135
138
|
The user does not need to send another chat message. Progress updates and status
|
|
136
139
|
notifications are also sent to clients that display them.
|
|
137
140
|
|
|
141
|
+
If the client suppresses or cannot display the prompt, or it is dismissed,
|
|
142
|
+
the server automatically opens a temporary local browser form and returns
|
|
143
|
+
`ACCESS_INPUT_REQUIRED` with its link. Show the link as a backup if the form
|
|
144
|
+
did not appear on the computer running the server.
|
|
145
|
+
Do not open that link again or ask for the email in chat while the browser form
|
|
146
|
+
is active. An accepted chat email prompt does not open a browser input form;
|
|
147
|
+
repeated tool calls reuse the active prompt or pending verification.
|
|
148
|
+
The human enters their email, opens the sign-in link from their inbox, and clicks
|
|
149
|
+
**Verify this device**. Do not fill
|
|
150
|
+
the form or open their email for them. The form expires after 10 minutes.
|
|
151
|
+
After they submit it, call `chrome_status` to wait for verification.
|
|
152
|
+
YOLO/bypass permission mode does not skip email verification.
|
|
153
|
+
|
|
154
|
+
Website access also needs Chrome permission. Relay the tool's one-line Allow
|
|
155
|
+
instructions; the person clicks Allow in the popup and accepts Chrome's native
|
|
156
|
+
prompt if shown. A server allow-domain flag does not supply this permission.
|
|
157
|
+
For a blocked optional feature, relay the Extra permissions instructions. Never
|
|
158
|
+
request or grant these permissions through browser automation.
|
|
159
|
+
|
|
138
160
|
If the human explicitly configured `--no-email-verification`, there is no email
|
|
139
|
-
step; continue to pairing. This option is intended only for trusted local
|
|
161
|
+
step; continue to pairing and Chrome client approval. This option is intended only for trusted local
|
|
140
162
|
clients and cannot be combined with a public tunnel.
|
|
141
163
|
|
|
142
164
|
Each verification wait lasts up to 45 seconds. If the result says
|
|
@@ -146,9 +168,7 @@ remain blocked until access is confirmed. Cancellation stops the waiting action,
|
|
|
146
168
|
while background verification can still activate the local installation.
|
|
147
169
|
|
|
148
170
|
Click **Connect** in the extension if the result says Chrome is not already
|
|
149
|
-
paired.
|
|
150
|
-
support elicitation; clients that cannot show the prompt cannot activate
|
|
151
|
-
through this flow.
|
|
171
|
+
paired.
|
|
152
172
|
|
|
153
173
|
The server also writes a `pairing.json` file for Load unpacked installs, which
|
|
154
174
|
may connect automatically. For the Chrome Web Store copy, or whenever Chrome
|
|
@@ -156,6 +176,19 @@ is still unpaired, the human clicks **Connect** in the extension popup after
|
|
|
156
176
|
email verification. If Chrome reports no paired browser, follow that step and
|
|
157
177
|
retry the tool.
|
|
158
178
|
|
|
179
|
+
When the tool asks for Chrome approval, relay its friendly one-line steps with
|
|
180
|
+
the app name and key fingerprint. They click the extension's toolbar icon, review **App
|
|
181
|
+
connections**, compare the key, then click **Approve for 8 hours**. The same
|
|
182
|
+
controls are in Settings. Never click
|
|
183
|
+
Approve for them or attempt to change the extension's approval storage. Retry
|
|
184
|
+
the tool after they approve. YOLO, bypass-permission mode and
|
|
185
|
+
`--no-email-verification` do not disable this check.
|
|
186
|
+
|
|
187
|
+
Approval belongs to one app session and one Chrome profile. A restarted MCP
|
|
188
|
+
connection creates a new key; expired access requires approval again. The person
|
|
189
|
+
can revoke one key from the popup or Settings without revoking the others. Update both the
|
|
190
|
+
server and extension; old versions cannot silently bypass this gate.
|
|
191
|
+
|
|
159
192
|
## Step 6 — Check the pairing
|
|
160
193
|
|
|
161
194
|
Pairing needs no paste. Every time the server boots it writes `pairing.json`
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare const ACCESS_STOPPED_KEY = "accessStopped";
|
|
2
|
+
export declare const ACTIVITY_KEY = "recentActivity";
|
|
3
|
+
export declare const ACTIVITY_LIMIT = 100;
|
|
4
|
+
export type ActivityOutcome = 'running' | 'done' | 'blocked' | 'failed' | 'interrupted';
|
|
5
|
+
export interface ActivityEntry {
|
|
6
|
+
id: string;
|
|
7
|
+
at: number;
|
|
8
|
+
method: string;
|
|
9
|
+
host: string;
|
|
10
|
+
/** Browser-session tab identity, used only to pair opening and closing. */
|
|
11
|
+
tabId?: number;
|
|
12
|
+
app: string;
|
|
13
|
+
key: string;
|
|
14
|
+
outcome: ActivityOutcome;
|
|
15
|
+
}
|
|
16
|
+
/** Deliberately excludes paths, queries, fragments, credentials and page data. */
|
|
17
|
+
export declare function activityHost(url: string): string;
|
|
18
|
+
/** Reconstruct a fixed schema; never spread caller-supplied command data into storage. */
|
|
19
|
+
export declare function activityEntry(value: ActivityEntry): ActivityEntry;
|
|
20
|
+
export interface ActivityRow extends ActivityEntry {
|
|
21
|
+
opened?: ActivityEntry;
|
|
22
|
+
}
|
|
23
|
+
/** Pair completed actions on the same tab and app; never merge by website alone. */
|
|
24
|
+
export declare function activityRows(entries: ActivityEntry[]): ActivityRow[];
|
|
25
|
+
export declare const activityLabel: (method: string) => string;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.activityLabel = exports.ACTIVITY_LIMIT = exports.ACTIVITY_KEY = exports.ACCESS_STOPPED_KEY = void 0;
|
|
4
|
+
exports.activityHost = activityHost;
|
|
5
|
+
exports.activityEntry = activityEntry;
|
|
6
|
+
exports.activityRows = activityRows;
|
|
7
|
+
const protocol_1 = require("./protocol");
|
|
8
|
+
const client_auth_1 = require("./client-auth");
|
|
9
|
+
exports.ACCESS_STOPPED_KEY = 'accessStopped';
|
|
10
|
+
exports.ACTIVITY_KEY = 'recentActivity';
|
|
11
|
+
exports.ACTIVITY_LIMIT = 100;
|
|
12
|
+
/** Deliberately excludes paths, queries, fragments, credentials and page data. */
|
|
13
|
+
function activityHost(url) {
|
|
14
|
+
if (url === 'about:blank')
|
|
15
|
+
return url;
|
|
16
|
+
if (url === 'chrome://newtab/' || url === 'chrome://newtab' || url === 'chrome://new-tab-page/')
|
|
17
|
+
return 'chrome://newtab';
|
|
18
|
+
try {
|
|
19
|
+
const parsed = new URL(url);
|
|
20
|
+
return ['http:', 'https:'].includes(parsed.protocol) ? parsed.hostname.slice(0, 253) : '';
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Reconstruct a fixed schema; never spread caller-supplied command data into storage. */
|
|
27
|
+
function activityEntry(value) {
|
|
28
|
+
return {
|
|
29
|
+
id: value.id.slice(0, 80),
|
|
30
|
+
at: value.at,
|
|
31
|
+
method: protocol_1.WIRE_METHODS.includes(value.method) ? value.method : 'unknown',
|
|
32
|
+
host: activityHost(value.host) || activityHost(`https://${value.host}/`),
|
|
33
|
+
...(Number.isSafeInteger(value.tabId) && value.tabId >= 0 ? { tabId: value.tabId } : {}),
|
|
34
|
+
app: value.key ? (0, client_auth_1.clientName)(value.app) : 'Unverified app',
|
|
35
|
+
key: /^[a-f0-9]{64}$/.test(value.key) ? value.key : '',
|
|
36
|
+
outcome: ['running', 'done', 'blocked', 'failed', 'interrupted'].includes(value.outcome) ? value.outcome : 'failed',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** Pair completed actions on the same tab and app; never merge by website alone. */
|
|
40
|
+
function activityRows(entries) {
|
|
41
|
+
const rows = [];
|
|
42
|
+
const opens = new Map();
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
const key = entry.tabId !== undefined && entry.key ? `${entry.key}:${entry.tabId}` : '';
|
|
45
|
+
if (key && entry.method === 'tab_new' && entry.outcome === 'done')
|
|
46
|
+
opens.set(key, entry);
|
|
47
|
+
const opened = key && entry.method === 'tab_close' && entry.outcome === 'done' ? opens.get(key) : undefined;
|
|
48
|
+
if (opened) {
|
|
49
|
+
const index = rows.findIndex((row) => row.id === opened.id);
|
|
50
|
+
if (index !== -1)
|
|
51
|
+
rows.splice(index, 1);
|
|
52
|
+
rows.push({ ...entry, host: entry.host || opened.host, opened });
|
|
53
|
+
opens.delete(key);
|
|
54
|
+
}
|
|
55
|
+
else
|
|
56
|
+
rows.push(entry);
|
|
57
|
+
}
|
|
58
|
+
return rows;
|
|
59
|
+
}
|
|
60
|
+
const LABELS = {
|
|
61
|
+
tabs_list: 'List tabs', tab_select: 'Select tab', tab_new: 'Open tab', tab_close: 'Close tab',
|
|
62
|
+
navigate: 'Navigate', back: 'Go back', forward: 'Go forward', reload: 'Reload page',
|
|
63
|
+
click: 'Click', draw: 'Draw', type: 'Type text', press: 'Press key', hover: 'Hover',
|
|
64
|
+
scroll: 'Scroll', screenshot: 'Take screenshot', get_text: 'Read text', get_html: 'Read HTML',
|
|
65
|
+
eval: 'Run page script', snapshot: 'Read page structure', fill_form: 'Fill form',
|
|
66
|
+
get_cookies: 'Read cookies', storage: 'Access site storage', download_file: 'Download file',
|
|
67
|
+
upload_file: 'Upload file', print_pdf: 'Print PDF', select_option: 'Choose option',
|
|
68
|
+
observers: 'Read page observations', proxy_status: 'Check proxy', frames: 'List frames',
|
|
69
|
+
};
|
|
70
|
+
const activityLabel = (method) => LABELS[method] ?? method.replaceAll('_', ' ');
|
|
71
|
+
exports.activityLabel = activityLabel;
|
|
72
|
+
//# sourceMappingURL=activity.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Only named HTTP(S) sites may be requested; never a catch-all or a file URL. */
|
|
2
|
+
export declare function siteOrigins(input: string): string[];
|
|
3
|
+
/** Match patterns have no port or path scope for host permissions. */
|
|
4
|
+
export declare function originForUrl(url: string): string | null;
|
|
5
|
+
export declare function isGlobalOrigin(origin: string): boolean;
|
|
6
|
+
export declare const OPTIONAL_FEATURES: {
|
|
7
|
+
readonly cookies: {
|
|
8
|
+
readonly label: "Cookies";
|
|
9
|
+
readonly explanation: "Read cookies on websites you have allowed. Cookie values still follow your server settings.";
|
|
10
|
+
};
|
|
11
|
+
readonly downloads: {
|
|
12
|
+
readonly label: "Downloads";
|
|
13
|
+
readonly explanation: "Save files from websites you have allowed.";
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
export type OptionalFeature = keyof typeof OPTIONAL_FEATURES;
|
|
17
|
+
export declare const FEATURE_REQUESTS_KEY = "browserPermissionRequests";
|
|
18
|
+
export declare const PERMISSIONS_MIGRATION_KEY = "optionalBrowserPermissionsV1";
|
|
19
|
+
export declare const PROXY_AUTH_PERMISSIONS: string[];
|
|
20
|
+
export declare const IP_LOOKUP_ORIGINS: string[];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IP_LOOKUP_ORIGINS = exports.PROXY_AUTH_PERMISSIONS = exports.PERMISSIONS_MIGRATION_KEY = exports.FEATURE_REQUESTS_KEY = exports.OPTIONAL_FEATURES = void 0;
|
|
4
|
+
exports.siteOrigins = siteOrigins;
|
|
5
|
+
exports.originForUrl = originForUrl;
|
|
6
|
+
exports.isGlobalOrigin = isGlobalOrigin;
|
|
7
|
+
const policy_1 = require("./policy");
|
|
8
|
+
/** Only named HTTP(S) sites may be requested; never a catch-all or a file URL. */
|
|
9
|
+
function siteOrigins(input) {
|
|
10
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(input) && !/^https?:\/\//i.test(input))
|
|
11
|
+
return [];
|
|
12
|
+
const pattern = (0, policy_1.normalizeDomainPattern)(input);
|
|
13
|
+
const host = pattern.startsWith('*.') ? pattern.slice(2) : pattern;
|
|
14
|
+
if (!host || host.length > 253 ||
|
|
15
|
+
!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/.test(host))
|
|
16
|
+
return [];
|
|
17
|
+
return [`http://${pattern}/*`, `https://${pattern}/*`];
|
|
18
|
+
}
|
|
19
|
+
/** Match patterns have no port or path scope for host permissions. */
|
|
20
|
+
function originForUrl(url) {
|
|
21
|
+
try {
|
|
22
|
+
const parsed = new URL(url);
|
|
23
|
+
return /^https?:$/.test(parsed.protocol) ? `${parsed.protocol}//${parsed.hostname}/*` : null;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function isGlobalOrigin(origin) {
|
|
30
|
+
return origin === '<all_urls>' || /^(?:\*|[a-z]+):\/\/\*\/.*$/i.test(origin);
|
|
31
|
+
}
|
|
32
|
+
exports.OPTIONAL_FEATURES = {
|
|
33
|
+
cookies: { label: 'Cookies', explanation: 'Read cookies on websites you have allowed. Cookie values still follow your server settings.' },
|
|
34
|
+
downloads: { label: 'Downloads', explanation: 'Save files from websites you have allowed.' },
|
|
35
|
+
};
|
|
36
|
+
exports.FEATURE_REQUESTS_KEY = 'browserPermissionRequests';
|
|
37
|
+
exports.PERMISSIONS_MIGRATION_KEY = 'optionalBrowserPermissionsV1';
|
|
38
|
+
exports.PROXY_AUTH_PERMISSIONS = ['webRequest', 'webRequestAuthProvider'];
|
|
39
|
+
exports.IP_LOOKUP_ORIGINS = ['https://ipinfo.io/*', 'https://api.ipify.org/*'];
|
|
40
|
+
//# sourceMappingURL=browser-permissions.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Per-session authorization, enforced by the extension independently of the bridge token. */
|
|
2
|
+
import type { CommandFrame } from './protocol';
|
|
3
|
+
export declare const CLIENT_APPROVAL_CAP = "client-approval-v1";
|
|
4
|
+
export declare const CLIENT_GRANT_MS: number;
|
|
5
|
+
export declare const CLIENT_CLOCK_SKEW_MS: number;
|
|
6
|
+
export declare const CLIENT_REQUEST_MS: number;
|
|
7
|
+
export declare const CLIENT_STORAGE_KEY = "clientConnections";
|
|
8
|
+
export interface ClientProof {
|
|
9
|
+
publicKey: string;
|
|
10
|
+
name: string;
|
|
11
|
+
challenge: string;
|
|
12
|
+
issuedAt: number;
|
|
13
|
+
nonce: string;
|
|
14
|
+
signature: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ClientConnection {
|
|
17
|
+
id: string;
|
|
18
|
+
publicKey: string;
|
|
19
|
+
name: string;
|
|
20
|
+
state: 'pending' | 'approved' | 'revoked';
|
|
21
|
+
requestedAt: number;
|
|
22
|
+
expiresAt: number;
|
|
23
|
+
}
|
|
24
|
+
export declare function clientName(value: string): string;
|
|
25
|
+
/** Correlation id is assigned by the hub; every executable field is signed. */
|
|
26
|
+
export declare function clientSigningPayload(cmd: Pick<CommandFrame, 'v' | 'method' | 'params' | 'tabId' | 'timeoutMs'>, proof: Omit<ClientProof, 'signature'>): string;
|
|
27
|
+
/** The only unsigned command allowed returns a fixed health response, without a tab lookup. */
|
|
28
|
+
export declare function isHealthProbe(cmd: Pick<CommandFrame, 'method' | 'params'>): boolean;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CLIENT_STORAGE_KEY = exports.CLIENT_REQUEST_MS = exports.CLIENT_CLOCK_SKEW_MS = exports.CLIENT_GRANT_MS = exports.CLIENT_APPROVAL_CAP = void 0;
|
|
4
|
+
exports.clientName = clientName;
|
|
5
|
+
exports.clientSigningPayload = clientSigningPayload;
|
|
6
|
+
exports.isHealthProbe = isHealthProbe;
|
|
7
|
+
exports.CLIENT_APPROVAL_CAP = 'client-approval-v1';
|
|
8
|
+
exports.CLIENT_GRANT_MS = 8 * 60 * 60 * 1000;
|
|
9
|
+
exports.CLIENT_CLOCK_SKEW_MS = 2 * 60 * 1000;
|
|
10
|
+
exports.CLIENT_REQUEST_MS = 10 * 60 * 1000;
|
|
11
|
+
exports.CLIENT_STORAGE_KEY = 'clientConnections';
|
|
12
|
+
function clientName(value) {
|
|
13
|
+
return value.replace(/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/g, '').trim().slice(0, 80) || 'Unnamed app';
|
|
14
|
+
}
|
|
15
|
+
/** Normalize as JSON first: omitted properties must sign identically across the wire. */
|
|
16
|
+
function canonical(value) {
|
|
17
|
+
if (Array.isArray(value))
|
|
18
|
+
return `[${value.map(canonical).join(',')}]`;
|
|
19
|
+
if (value && typeof value === 'object') {
|
|
20
|
+
const obj = value;
|
|
21
|
+
return `{${Object.keys(obj).sort().map((k) => `${JSON.stringify(k)}:${canonical(obj[k])}`).join(',')}}`;
|
|
22
|
+
}
|
|
23
|
+
return JSON.stringify(value);
|
|
24
|
+
}
|
|
25
|
+
/** Correlation id is assigned by the hub; every executable field is signed. */
|
|
26
|
+
function clientSigningPayload(cmd, proof) {
|
|
27
|
+
return canonical(JSON.parse(JSON.stringify({
|
|
28
|
+
domain: exports.CLIENT_APPROVAL_CAP, v: cmd.v, method: cmd.method, params: cmd.params,
|
|
29
|
+
tabId: cmd.tabId ?? null, timeoutMs: cmd.timeoutMs,
|
|
30
|
+
publicKey: proof.publicKey, name: proof.name, challenge: proof.challenge,
|
|
31
|
+
issuedAt: proof.issuedAt, nonce: proof.nonce,
|
|
32
|
+
})));
|
|
33
|
+
}
|
|
34
|
+
/** The only unsigned command allowed returns a fixed health response, without a tab lookup. */
|
|
35
|
+
function isHealthProbe(cmd) {
|
|
36
|
+
return cmd.method === 'ping_probe' && Object.keys(cmd.params ?? {}).length === 0;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=client-auth.js.map
|
|
@@ -103,7 +103,7 @@ export declare function readObservers(globalName: string, opts: {
|
|
|
103
103
|
* Translate the domain allowlist into Chrome match patterns for registering the
|
|
104
104
|
* hook as a content script. A bare host becomes `*://host/*`; `*.host` becomes
|
|
105
105
|
* `*://*.host/*` (which Chrome reads as the host AND its subdomains, matching
|
|
106
|
-
* how the allowlist itself behaves)
|
|
106
|
+
* how the allowlist itself behaves). Catch-all entries and entries carrying
|
|
107
107
|
* no host are dropped rather than widened — the page the tool may not read is
|
|
108
108
|
* also the page it must not instrument.
|
|
109
109
|
*/
|
package/dist/shared/observers.js
CHANGED
|
@@ -106,7 +106,7 @@ function readObservers(globalName, opts) {
|
|
|
106
106
|
* Translate the domain allowlist into Chrome match patterns for registering the
|
|
107
107
|
* hook as a content script. A bare host becomes `*://host/*`; `*.host` becomes
|
|
108
108
|
* `*://*.host/*` (which Chrome reads as the host AND its subdomains, matching
|
|
109
|
-
* how the allowlist itself behaves)
|
|
109
|
+
* how the allowlist itself behaves). Catch-all entries and entries carrying
|
|
110
110
|
* no host are dropped rather than widened — the page the tool may not read is
|
|
111
111
|
* also the page it must not instrument.
|
|
112
112
|
*/
|
|
@@ -118,8 +118,6 @@ function observerMatches(policy) {
|
|
|
118
118
|
const p = (0, policy_1.normalizeDomainPattern)(raw);
|
|
119
119
|
if (!p)
|
|
120
120
|
continue;
|
|
121
|
-
if (p === '*')
|
|
122
|
-
return ['<all_urls>'];
|
|
123
121
|
out.add(`*://${p}/*`);
|
|
124
122
|
}
|
|
125
123
|
return [...out];
|
package/dist/shared/policy.d.ts
CHANGED
|
@@ -22,8 +22,8 @@ export declare function hostOf(url: string): string;
|
|
|
22
22
|
* a full URL ("https://example.com/app") or a "host:port/path" instead of a bare
|
|
23
23
|
* host; those forms would never equal a hostname and so silently match nothing.
|
|
24
24
|
* We strip the scheme, userinfo, port, and path/query/fragment, preserving a
|
|
25
|
-
* leading "*."
|
|
26
|
-
*
|
|
25
|
+
* leading "*." for a named site's subdomains. Catch-all patterns and patterns
|
|
26
|
+
* without a host return '' and never grant access.
|
|
27
27
|
*/
|
|
28
28
|
export declare function normalizeDomainPattern(pattern: string): string;
|
|
29
29
|
export declare function isDomainAllowed(url: string, policy: WirePolicy): boolean;
|
|
@@ -40,11 +40,9 @@ export type PolicyVerdict = {
|
|
|
40
40
|
*/
|
|
41
41
|
export declare function evaluatePolicy(url: string, method: WireMethod, policy: WirePolicy): PolicyVerdict;
|
|
42
42
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* the exact one-line change to permit this one — so a non-technical user is never
|
|
46
|
-
* left at a dead end. Never includes the token or any other secret.
|
|
43
|
+
* One friendly line with the person's next step. The agent must relay it and
|
|
44
|
+
* wait; only the person can approve the site in Chrome.
|
|
47
45
|
*/
|
|
48
|
-
export declare function blockedDomainMessage(
|
|
46
|
+
export declare function blockedDomainMessage(host: string): string;
|
|
49
47
|
/** A wire policy that allows nothing — the safe default when none was delivered. */
|
|
50
48
|
export declare const DENY_ALL_WIRE_POLICY: WirePolicy;
|
package/dist/shared/policy.js
CHANGED
|
@@ -103,28 +103,26 @@ function isAboutBlank(url) {
|
|
|
103
103
|
* a full URL ("https://example.com/app") or a "host:port/path" instead of a bare
|
|
104
104
|
* host; those forms would never equal a hostname and so silently match nothing.
|
|
105
105
|
* We strip the scheme, userinfo, port, and path/query/fragment, preserving a
|
|
106
|
-
* leading "*."
|
|
107
|
-
*
|
|
106
|
+
* leading "*." for a named site's subdomains. Catch-all patterns and patterns
|
|
107
|
+
* without a host return '' and never grant access.
|
|
108
108
|
*/
|
|
109
109
|
function normalizeDomainPattern(pattern) {
|
|
110
110
|
let p = pattern.trim().toLowerCase();
|
|
111
|
-
if (p === '*' || p === '*://*/*')
|
|
112
|
-
return '
|
|
111
|
+
if (p === '*' || p === '*://*/*' || p === '<all_urls>')
|
|
112
|
+
return '';
|
|
113
113
|
// Strip a leading scheme ("https://", "http://", any "scheme://").
|
|
114
114
|
p = p.replace(/^[a-z][a-z0-9+.-]*:\/\//, '');
|
|
115
115
|
// Drop everything from the first path/query/fragment separator onward.
|
|
116
116
|
p = p.replace(/[/?#].*$/, '');
|
|
117
117
|
// Strip userinfo ("user:pass@") then a trailing port (":8080").
|
|
118
118
|
p = p.replace(/^[^@]*@/, '').replace(/:\d+$/, '');
|
|
119
|
-
return p;
|
|
119
|
+
return p === '*' || p === '*.' ? '' : p;
|
|
120
120
|
}
|
|
121
|
-
/** Convert a
|
|
121
|
+
/** Convert a named domain pattern to a predicate.
|
|
122
122
|
* '*.example.com' matches example.com and any subdomain; otherwise exact host.
|
|
123
123
|
* URL/port/path forms are normalized to their bare host first. */
|
|
124
124
|
function globMatches(host, pattern) {
|
|
125
125
|
const p = normalizeDomainPattern(pattern);
|
|
126
|
-
if (p === '*')
|
|
127
|
-
return true;
|
|
128
126
|
if (p.startsWith('*.')) {
|
|
129
127
|
const base = p.slice(2);
|
|
130
128
|
return host === base || host.endsWith('.' + base);
|
|
@@ -179,29 +177,16 @@ function evaluatePolicy(url, method, policy) {
|
|
|
179
177
|
return { ok: true };
|
|
180
178
|
if (!isDomainAllowed(url, policy)) {
|
|
181
179
|
const host = hostOf(url) || url;
|
|
182
|
-
return { ok: false, reason: blockedDomainMessage(
|
|
180
|
+
return { ok: false, reason: blockedDomainMessage(host) };
|
|
183
181
|
}
|
|
184
182
|
return { ok: true };
|
|
185
183
|
}
|
|
186
184
|
/**
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
* the exact one-line change to permit this one — so a non-technical user is never
|
|
190
|
-
* left at a dead end. Never includes the token or any other secret.
|
|
185
|
+
* One friendly line with the person's next step. The agent must relay it and
|
|
186
|
+
* wait; only the person can approve the site in Chrome.
|
|
191
187
|
*/
|
|
192
|
-
function blockedDomainMessage(
|
|
193
|
-
|
|
194
|
-
const allowedLine = allowed.length
|
|
195
|
-
? `Currently allowed: ${allowed.join(', ')}.`
|
|
196
|
-
: `Right now no sites are allowed.`;
|
|
197
|
-
return (`Blocked: "${method}" can't run on ${host} because it isn't on this browser tool's allowed-sites list. ` +
|
|
198
|
-
`This is a safety limit (the tool drives your real, logged-in browser, so it only touches sites you've approved) — not an error. ` +
|
|
199
|
-
`${allowedLine} ` +
|
|
200
|
-
`To allow ${host} right now, ask the user to click the MCP Browser Extension's toolbar icon in Chrome ` +
|
|
201
|
-
`and click Allow next to ${host}: it applies at once, no restart. ` +
|
|
202
|
-
`You cannot approve a site yourself. ` +
|
|
203
|
-
`Or add --allow-domain "${host}" (or "*.${host}" to include subdomains) to the MCP Browser Extension server settings (its entry in your MCP config) and restart. ` +
|
|
204
|
-
`To allow every site (less safe), use --unsafe-all-domains.`);
|
|
188
|
+
function blockedDomainMessage(host) {
|
|
189
|
+
return `Please allow access to ${host}: Chrome → MCP Browser Extension icon → Allow next to ${host}, then try again.`;
|
|
205
190
|
}
|
|
206
191
|
/** A wire policy that allows nothing — the safe default when none was delivered. */
|
|
207
192
|
exports.DENY_ALL_WIRE_POLICY = {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ClientProof } from './client-auth';
|
|
1
2
|
/**
|
|
2
3
|
* shared/protocol.ts — THE SINGLE SOURCE OF TRUTH for the wire contract.
|
|
3
4
|
*
|
|
@@ -132,6 +133,8 @@ export interface BaseFrame {
|
|
|
132
133
|
v: ProtocolVersion;
|
|
133
134
|
}
|
|
134
135
|
export interface HelloFrame extends BaseFrame {
|
|
136
|
+
/** Random challenge chosen by the extension for this WebSocket, never by the hub. */
|
|
137
|
+
approvalChallenge?: string;
|
|
135
138
|
type: 'hello';
|
|
136
139
|
token: string;
|
|
137
140
|
ext: {
|
|
@@ -200,6 +203,7 @@ export interface UnauthFrame extends BaseFrame {
|
|
|
200
203
|
reason: 'bad_token' | 'bad_version' | 'timeout';
|
|
201
204
|
}
|
|
202
205
|
export interface CommandFrame extends BaseFrame {
|
|
206
|
+
authorization?: ClientProof;
|
|
203
207
|
type: 'command';
|
|
204
208
|
id: string;
|
|
205
209
|
method: WireMethod;
|