chrome-bridge-mcp 1.13.0 → 1.14.0
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 +112 -129
- package/extension/manifest.json +1 -1
- package/extension/service-worker.js +51 -9
- package/package.json +1 -1
- package/server/protocol.js +1 -1
- package/server/tools.js +20 -8
package/README.md
CHANGED
|
@@ -1,166 +1,141 @@
|
|
|
1
1
|
# Chrome Bridge
|
|
2
2
|
|
|
3
|
-
       [](https://chromewebstore.google.com/detail/chrome-bridge-for-claude/bioknpaeahidbelaljjohjofiloeodmb)
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**An MCP server that gives Claude Code your real, logged-in Chrome — measured
|
|
6
|
+
2.75× fewer turns and 2.28× lower cost than the official "Claude in Chrome"
|
|
7
|
+
extension on a form-filling task, with ~3× the toolset and no paid plan.**
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
62 web-development tools (navigation, DOM inspection, visual regression, audits,
|
|
10
|
+
network mocking) over a local WebSocket bridge, plus a headless instance for CI.
|
|
11
|
+
Self-hosted, local-only.
|
|
8
12
|
|
|
9
|
-
##
|
|
13
|
+
## Quickstart
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
**Requires** Node.js 18+ and Chrome 135+.
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
17
|
+
```bash
|
|
18
|
+
git clone git@github.com:frsorrentino/chrome-bridge.git
|
|
19
|
+
cd chrome-bridge && ./install.sh
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
1. Open `chrome://extensions`, enable **Developer mode**, click **Load
|
|
23
|
+
unpacked**, select the `extension/` folder.
|
|
24
|
+
2. Restart Claude Code.
|
|
15
25
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
26
|
+
Then ask for something like *"open localhost:3000, run an accessibility audit
|
|
27
|
+
and find the Sign Up button"*: Claude Code calls `navigate`,
|
|
28
|
+
`accessibility_audit` and `find_text`. Because `navigate` already returns
|
|
29
|
+
element refs, `click(ref="n1")` follows with no discovery turn in between.
|
|
20
30
|
|
|
21
|
-
On
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
[bench/RESULTS.md](bench/RESULTS.md).
|
|
31
|
+
> **On ChromeOS/Crostini** install from the [Chrome Web
|
|
32
|
+
> Store](https://chromewebstore.google.com/detail/chrome-bridge-for-claude/bioknpaeahidbelaljjohjofiloeodmb)
|
|
33
|
+
> instead: an unpacked extension is dropped on every reboot, because the
|
|
34
|
+
> container isn't mounted when Chrome starts.
|
|
26
35
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
- **Honest caveat:** per *turn*, Chrome Bridge costs slightly more than Claude in Chrome (41,985 vs 36,613 cache-read tokens; $0.0373 vs $0.0337). The win is in the number of turns.
|
|
36
|
+
`install.sh` registers the MCP server with `--scope user`. To do it by hand:
|
|
37
|
+
`claude mcp add --scope user chrome-bridge node /path/to/server/index.js`.
|
|
38
|
+
For `execute_js`, enable **Allow user scripts** in `chrome://extensions` →
|
|
39
|
+
Chrome Bridge → Details (on Chrome 135-137, enable Developer Mode instead).
|
|
32
40
|
|
|
33
|
-
|
|
34
|
-
Chrome Bridge ships 62 specialized tools (33 core by default) compared to ~20 in Claude in Chrome. It is also the only automation that drives the real host Chrome on ChromeOS/Crostini.
|
|
41
|
+
## Why Chrome Bridge?
|
|
35
42
|
|
|
36
43
|
| | Chrome Bridge | Claude in Chrome | Chrome DevTools MCP | Playwright MCP |
|
|
37
44
|
|---|---|---|---|---|
|
|
38
45
|
| **ChromeOS / Crostini** | **Yes** (real host) | No | Container only | Container only |
|
|
39
46
|
| **Tools** | **62** (33 core) | ~20 | ~50 | 23 core (71 total) |
|
|
40
|
-
| **Requires
|
|
41
|
-
| **Network
|
|
42
|
-
| **Visual
|
|
43
|
-
| **Audits (
|
|
47
|
+
| **Requires paid plan** | **No** | Yes (Pro+) | No | No |
|
|
48
|
+
| **Network mocking** | **Yes** (stub/headers) | No | No | Yes |
|
|
49
|
+
| **Visual regression** | **Yes** (`screenshot_diff`) | No | No | No |
|
|
50
|
+
| **Audits (a11y/SEO/sec)** | **Yes** (full suite) | No | Partial | No |
|
|
44
51
|
| **Headless / CI** | **Yes** | No | Yes | Yes |
|
|
45
|
-
| **GIF /
|
|
46
|
-
| **Breakpoints /
|
|
47
|
-
|
|
48
|
-
## Quickstart
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
git clone git@github.com:frsorrentino/chrome-bridge.git && cd chrome-bridge && ./install.sh
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
1. Load the extension: Open `chrome://extensions`, enable **Developer mode**, click **Load unpacked**, and select the `extension/` folder.
|
|
55
|
-
2. Restart Claude Code.
|
|
56
|
-
|
|
57
|
-
> On **ChromeOS/Crostini**, install the published extension from the [Chrome Web Store](https://chromewebstore.google.com/detail/chrome-bridge-for-claude/bioknpaeahidbelaljjohjofiloeodmb) instead of loading it unpacked — a filesystem-loaded extension is dropped on every reboot (the container isn't mounted when Chrome starts).
|
|
58
|
-
|
|
59
|
-
### Example Session
|
|
60
|
-
> *"Open localhost:3000, run an accessibility audit, and find the 'Sign Up' button"*
|
|
61
|
-
|
|
62
|
-
Claude Code calls `navigate` (returning clickable element refs), `accessibility_audit`, and `find_text`. Because `navigate` returns refs like `n1`, the agent can immediately call `click(ref="n1")` without a discovery turn.
|
|
52
|
+
| **GIF / video** | No | **Yes** | Partial | No |
|
|
53
|
+
| **Breakpoints / heap** | No | No | **Yes** | No |
|
|
63
54
|
|
|
64
|
-
|
|
55
|
+
It wins on **round trips, not payload size**: short element refs instead of the
|
|
56
|
+
screenshot-and-click loop, `fill_form` filling N fields in one call, table
|
|
57
|
+
filtering done server-side. Per single turn it actually costs slightly *more*.
|
|
65
58
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
- **Payload Escape Hatches**: `save_to` on `read_page`, `extract`, `screenshot` and `http_request` writes the result to a file and returns the path — the bytes never enter the context unless the agent decides to read them. `read_page(mode="markdown")` keeps headings, links and tables at a fraction of the HTML cost. The [CLI](#cli) skips MCP schemas entirely, and recorded flows [replay](#launch-mode-headless--ci) without any model in the loop.
|
|
59
|
+
The full benchmark — method, every raw run including the unfavourable ones, and
|
|
60
|
+
what the harness can't measure — is in
|
|
61
|
+
[docs/EFFICIENCY.md](docs/EFFICIENCY.md).
|
|
70
62
|
|
|
71
|
-
##
|
|
63
|
+
## Using it
|
|
72
64
|
|
|
73
|
-
|
|
74
|
-
- **Network Mocking**: Block/redirect requests, rewrite headers, or stub response bodies with synthetic data.
|
|
75
|
-
- **Visual Regression**: Use `screenshot_diff` to compare current pages against named baselines.
|
|
76
|
-
- **Structured Extraction**: One-call `extract` for repeated data and `extract_table` with server-side `where` filtering.
|
|
65
|
+
Beyond the MCP tools, two lanes keep work away from the model entirely.
|
|
77
66
|
|
|
78
|
-
|
|
67
|
+
**CLI** — batch operations, piped through `grep` or `jq` before anything reaches
|
|
68
|
+
the context:
|
|
79
69
|
|
|
70
|
+
```bash
|
|
71
|
+
chrome-bridge navigate --url https://example.com
|
|
72
|
+
chrome-bridge read_console --level error | head -20
|
|
73
|
+
chrome-bridge assert --selector "#success" --text "Done"
|
|
74
|
+
chrome-bridge replay --file ./recordings/login.jsonl
|
|
80
75
|
```
|
|
81
|
-
Claude Code <--stdio--> MCP Server <--WebSocket :8765--> Chrome Extension
|
|
82
|
-
(server/) (extension/, MV3)
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
The **MCP Server** (Node.js) handles the protocol and tool logic. The **Chrome Extension** (MV3) executes commands via Chrome APIs. User scripts (`execute_js`) run via `chrome.userScripts.execute()`, requiring the "Allow user scripts" toggle in extension settings.
|
|
86
|
-
|
|
87
|
-
## Tools (62 total)
|
|
88
76
|
|
|
89
|
-
|
|
90
|
-
|
|
77
|
+
**Launch mode** — a dedicated Chromium instance with an ephemeral profile, for
|
|
78
|
+
isolated sessions or CI:
|
|
91
79
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
### DOM & Inspection (11)
|
|
96
|
-
`read_page`, `extract`, `get_page_info`, `query_dom`, `modify_dom`, `find_text`, `get_interactives`, `inject_css`, `highlight_elements`, `watch_dom`, `measure_spacing`.
|
|
80
|
+
```bash
|
|
81
|
+
node server/index.js --launch --headless
|
|
82
|
+
```
|
|
97
83
|
|
|
98
|
-
|
|
99
|
-
|
|
84
|
+
Pair it with `session_record` + `replay` for smoke tests with no model in the
|
|
85
|
+
loop. In launch mode `execute_js` falls back to `new Function` when the
|
|
86
|
+
user-script toggle isn't available.
|
|
100
87
|
|
|
101
|
-
|
|
102
|
-
`element_screenshot`, `full_page_screenshot`, `screenshot_diff`, `viewport_resize`, `set_zoom`, `emulate_media`, `set_geolocation`.
|
|
88
|
+
## Tools
|
|
103
89
|
|
|
104
|
-
|
|
105
|
-
|
|
90
|
+
62 in total, in seven groups. Only `core` (33 tools) loads by default; the rest
|
|
91
|
+
are opt-in via `--caps`.
|
|
106
92
|
|
|
107
|
-
|
|
108
|
-
|
|
93
|
+
| Group | N | What's in it |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| Core & Navigation | 9 | tabs, windows, `navigate`, `screenshot`, `tile_windows` |
|
|
96
|
+
| Interaction | 11 | `click`, `fill_form`, `upload_file`, dialogs, clipboard |
|
|
97
|
+
| DOM & Inspection | 11 | `read_page`, `extract`, `query_dom`, `watch_dom` |
|
|
98
|
+
| Debugging & Network | 9 | `execute_js`, console, network log, mocking, Web Vitals |
|
|
99
|
+
| Visual & Responsive | 7 | `screenshot_diff`, viewport, zoom, media emulation |
|
|
100
|
+
| Audits | 6 | a11y, SEO, security headers, links, `extract_table` |
|
|
101
|
+
| State, Storage & Files | 9 | storage, fixtures, MHTML, recording, `assert` |
|
|
109
102
|
|
|
110
|
-
|
|
103
|
+
Every tool, with the notes that matter: [docs/TOOLS.md](docs/TOOLS.md).
|
|
111
104
|
|
|
112
|
-
|
|
105
|
+
## How it works
|
|
113
106
|
|
|
114
|
-
### From Source
|
|
115
|
-
```bash
|
|
116
|
-
git clone git@github.com:frsorrentino/chrome-bridge.git
|
|
117
|
-
cd chrome-bridge
|
|
118
|
-
./install.sh
|
|
119
107
|
```
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
### Manual Registration
|
|
123
|
-
```bash
|
|
124
|
-
claude mcp add --scope user chrome-bridge node /path/to/server/index.js
|
|
108
|
+
Claude Code <--stdio--> MCP Server <--WebSocket :8765--> Chrome Extension
|
|
109
|
+
(server/) (extension/, MV3)
|
|
125
110
|
```
|
|
126
111
|
|
|
127
|
-
|
|
112
|
+
The Node.js server handles the protocol and tool logic; the MV3 extension
|
|
113
|
+
executes commands through Chrome APIs. User scripts (`execute_js`) run via
|
|
114
|
+
`chrome.userScripts.execute()`.
|
|
128
115
|
|
|
129
|
-
## Configuration
|
|
116
|
+
## Configuration and security
|
|
130
117
|
|
|
131
|
-
|
|
132
|
-
- `CHROME_BRIDGE_PORT`: Default `8765`.
|
|
133
|
-
- `CHROME_BRIDGE_HOST` / `--host`: Bind address, default `127.0.0.1`. Set to `0.0.0.0` **only** where the browser lives outside the container (ChromeOS/Crostini port-forward) — and pair it with a token.
|
|
134
|
-
- `CHROME_BRIDGE_TOKEN`: Shared secret required on both `ext_init` and `relay_init`. Strongly recommended whenever the bind is not loopback.
|
|
135
|
-
- `CHROME_BRIDGE_CAPS` / `--caps`: Tool groups to load (`core`, `audits`, `visual`, `network`, `storage`, `dom`, `files`, or `all`). `install.sh` registers with `all`; the bare server defaults to `core` (33 tools). `get_status` reports `caps_active` and `caps_available`.
|
|
118
|
+
Environment variables, each with a matching CLI flag:
|
|
136
119
|
|
|
137
|
-
|
|
120
|
+
| Variable | Default | Notes |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `CHROME_BRIDGE_PORT` | `8765` | |
|
|
123
|
+
| `CHROME_BRIDGE_HOST` / `--host` | `127.0.0.1` | `0.0.0.0` **only** where the browser lives outside the container (ChromeOS/Crostini port-forward) — and only with a token |
|
|
124
|
+
| `CHROME_BRIDGE_TOKEN` | unset | Required on both `ext_init` and `relay_init`. Strongly recommended whenever the bind isn't loopback |
|
|
125
|
+
| `CHROME_BRIDGE_CAPS` / `--caps` | `core` | `core`, `audits`, `visual`, `network`, `storage`, `dom`, `files`, `all`. `install.sh` uses `all` |
|
|
138
126
|
|
|
139
|
-
|
|
127
|
+
The bridge binds loopback, accepts extension connections only from a
|
|
128
|
+
`chrome-extension://` origin, and — when a token is set — requires it on both
|
|
129
|
+
handshakes. Without one, any local process could act as a relay and reach
|
|
130
|
+
`execute_js` inside your authenticated browser session. Secondary MCP instances
|
|
131
|
+
connect via loopback and are acknowledged with `relay_init_ok`, so a foreign
|
|
132
|
+
process holding the port fails fast instead of timing out per command.
|
|
140
133
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
chrome-bridge replay --file ./recordings/login.jsonl
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
## Launch Mode (Headless / CI)
|
|
150
|
-
|
|
151
|
-
Launch mode starts a dedicated Chromium instance with an ephemeral profile:
|
|
152
|
-
```bash
|
|
153
|
-
node server/index.js --launch --headless
|
|
154
|
-
```
|
|
155
|
-
Perfect for CI smoke tests using `session_record` + `replay`. Note: `execute_js` uses a `new Function` fallback in launch mode if the user-script toggle is unavailable.
|
|
156
|
-
|
|
157
|
-
## Security
|
|
158
|
-
|
|
159
|
-
- **Loopback by default**: the WebSocket server binds `127.0.0.1`. Exposing it to the network is opt-in via `CHROME_BRIDGE_HOST`/`--host`.
|
|
160
|
-
- **Origin validation**: extension connections are only accepted from a `chrome-extension://` origin, and — with a loopback bind — only from loopback.
|
|
161
|
-
- **Token on both handshakes**: `CHROME_BRIDGE_TOKEN`, when set, is required for `ext_init` *and* `relay_init`. Without it, any local process could act as a relay and reach `execute_js` in your authenticated browser session.
|
|
162
|
-
- **Relay security**: secondary MCP instances must connect via loopback and are acknowledged with `relay_init_ok`, so a foreign process holding the port fails fast instead of timing out per command.
|
|
163
|
-
- **What is *not* protected**: page content reaches the model unfiltered, so a hostile page's text is untrusted input; `get_storage`, `session_fixture`, HAR exports and screenshots are **not** redacted and may carry cookies, tokens or personal data. Do not point the automation at pages holding secrets you would not paste into a chat.
|
|
134
|
+
**What is *not* protected:** page content reaches the model unfiltered, so a
|
|
135
|
+
hostile page's text is untrusted input. `get_storage`, `session_fixture`, HAR
|
|
136
|
+
exports and screenshots are **not** redacted and may carry cookies, tokens or
|
|
137
|
+
personal data. Don't point the automation at pages holding secrets you wouldn't
|
|
138
|
+
paste into a chat.
|
|
164
139
|
|
|
165
140
|
## Troubleshooting
|
|
166
141
|
|
|
@@ -170,16 +145,24 @@ Perfect for CI smoke tests using `session_record` + `replay`. Note: `execute_js`
|
|
|
170
145
|
| Port 8765 already in use | Expected: a second MCP session becomes a **relay** and shares the one bridge. Set `CHROME_BRIDGE_PORT` for a separate one. |
|
|
171
146
|
| `Port N is held by a process that is not chrome-bridge` | Something else owns the port. Free it or change `CHROME_BRIDGE_PORT`. |
|
|
172
147
|
| `execute_js` fails | Enable **Allow user scripts** in `chrome://extensions` → Chrome Bridge → Details (Chrome 138+; on 135-137 enable Developer Mode). |
|
|
173
|
-
| `read_console` returns
|
|
148
|
+
| `read_console` returns `note=Instrumentation not loaded` | The page was opened before the extension, "Capture console & metrics" is off, or the page isn't injectable (`chrome://`). Reload it. |
|
|
174
149
|
| Screenshot times out | On ChromeOS a fully occluded window stops producing frames; captures fail after 10s. Bring the window forward. |
|
|
175
|
-
| Commands work then stop
|
|
176
|
-
| Extension
|
|
177
|
-
| Tool missing from the list | It's in an opt-in
|
|
150
|
+
| Commands work, then stop | The MV3 service worker restarted and in-memory state (network log, diff baselines, HTTP auth) was reset. Re-run the monitoring call. |
|
|
151
|
+
| Extension dropped on every ChromeOS reboot | Install from the Web Store instead of Load unpacked. |
|
|
152
|
+
| Tool missing from the list | It's in an opt-in group. Check `get_status` → `caps_available`, then set `CHROME_BRIDGE_CAPS=all`. |
|
|
153
|
+
|
|
154
|
+
## Documentation
|
|
155
|
+
|
|
156
|
+
- [docs/TOOLS.md](docs/TOOLS.md) — all 62 tools, by group
|
|
157
|
+
- [docs/EFFICIENCY.md](docs/EFFICIENCY.md) — the benchmark and the design behind it
|
|
158
|
+
- [bench/RESULTS.md](bench/RESULTS.md) — raw runs and inclusion rule
|
|
159
|
+
- [CHANGELOG.md](CHANGELOG.md)
|
|
178
160
|
|
|
179
161
|
## Tests
|
|
180
|
-
|
|
181
|
-
-
|
|
182
|
-
|
|
162
|
+
|
|
163
|
+
`npm test` (Chrome-free, ~22s) · `npm run test:e2e` (needs Chrome and a
|
|
164
|
+
connected extension) · `npm run measure` (schema cost).
|
|
183
165
|
|
|
184
166
|
## License
|
|
167
|
+
|
|
185
168
|
MIT
|
package/extension/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Chrome Bridge for Claude Code",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.14.0",
|
|
5
5
|
"description": "Bridge your browser to Claude Code: 62 token-efficient web-dev automation tools over a local WebSocket. ChromeOS included.",
|
|
6
6
|
"homepage_url": "https://github.com/frsorrentino/chrome-bridge",
|
|
7
7
|
"minimum_chrome_version": "135",
|
|
@@ -322,7 +322,7 @@ async function executeCommand(msg) {
|
|
|
322
322
|
|
|
323
323
|
switch (type) {
|
|
324
324
|
case 'get_tabs':
|
|
325
|
-
return await cmdGetTabs();
|
|
325
|
+
return await cmdGetTabs(params);
|
|
326
326
|
case 'navigate':
|
|
327
327
|
return await cmdNavigate(params);
|
|
328
328
|
case 'screenshot':
|
|
@@ -649,15 +649,36 @@ async function bitmapToBase64Capped(bitmap, maxSide = MAX_IMAGE_SIDE) {
|
|
|
649
649
|
|
|
650
650
|
// --- Implementazione comandi ---
|
|
651
651
|
|
|
652
|
-
async function cmdGetTabs() {
|
|
652
|
+
async function cmdGetTabs({ include_windows = false } = {}) {
|
|
653
653
|
const tabs = await chrome.tabs.query({});
|
|
654
|
-
|
|
654
|
+
const list = tabs.map((t) => ({
|
|
655
655
|
id: t.id,
|
|
656
656
|
url: t.url,
|
|
657
657
|
title: t.title,
|
|
658
658
|
active: t.active,
|
|
659
659
|
windowId: t.windowId,
|
|
660
660
|
}));
|
|
661
|
+
if (!include_windows) return list;
|
|
662
|
+
|
|
663
|
+
// Senza geometria e stato non si può decidere dove mettere una finestra: si
|
|
664
|
+
// può solo indovinare. `left` in particolare è ciò che identifica il monitor
|
|
665
|
+
// su una scrivania virtuale multi-schermo.
|
|
666
|
+
const windows = (await chrome.windows.getAll({ populate: true })).map((w) => ({
|
|
667
|
+
id: w.id,
|
|
668
|
+
type: w.type,
|
|
669
|
+
state: w.state,
|
|
670
|
+
focused: w.focused,
|
|
671
|
+
left: w.left,
|
|
672
|
+
top: w.top,
|
|
673
|
+
width: w.width,
|
|
674
|
+
height: w.height,
|
|
675
|
+
tabs: (w.tabs || []).length,
|
|
676
|
+
// Una finestra le cui schede sono tutte chrome:// o chrome-untrusted:// non
|
|
677
|
+
// può fornire l'area del monitor a tile_windows: meglio saperlo prima di
|
|
678
|
+
// provarci.
|
|
679
|
+
scriptable: (w.tabs || []).some((t) => /^(https?|file):/i.test(t.url || '')),
|
|
680
|
+
}));
|
|
681
|
+
return { tabs: list, windows };
|
|
661
682
|
}
|
|
662
683
|
|
|
663
684
|
async function cmdNavigate({ url, tab_id }) {
|
|
@@ -965,7 +986,7 @@ async function cmdCreateTab({ url, active = true }) {
|
|
|
965
986
|
* chrome-untrusted:// non può fornire l'area, e il tool lo dice invece di
|
|
966
987
|
* ripiegare su misure inventate.
|
|
967
988
|
*/
|
|
968
|
-
async function cmdTileWindows({ window_ids, reference_window_id, layout = 'grid', padding = 0, include_types }) {
|
|
989
|
+
async function cmdTileWindows({ window_ids, reference_window_id, layout = 'grid', padding = 0, include_types, area: explicitArea }) {
|
|
969
990
|
const types = include_types?.length ? include_types : ['normal'];
|
|
970
991
|
const all = await chrome.windows.getAll({ populate: true });
|
|
971
992
|
|
|
@@ -986,9 +1007,12 @@ async function cmdTileWindows({ window_ids, reference_window_id, layout = 'grid'
|
|
|
986
1007
|
.flatMap((w) => (w.tabs || []).map((t) => ({ windowId: w.id, tabId: t.id, url: t.url || '' })))
|
|
987
1008
|
.filter((t) => /^(https?|file):/i.test(t.url));
|
|
988
1009
|
|
|
989
|
-
|
|
1010
|
+
// Un'area esplicita salta la lettura da una scheda: è l'unica strada quando
|
|
1011
|
+
// le finestre bersaglio hanno solo pagine interne di Chrome, come quelle del
|
|
1012
|
+
// Terminale ChromeOS.
|
|
1013
|
+
let area = explicitArea ?? null;
|
|
990
1014
|
const failures = [];
|
|
991
|
-
for (const cand of candidates) {
|
|
1015
|
+
for (const cand of (area ? [] : candidates)) {
|
|
992
1016
|
try {
|
|
993
1017
|
const [res] = await chrome.scripting.executeScript({
|
|
994
1018
|
target: { tabId: cand.tabId },
|
|
@@ -1809,7 +1833,7 @@ async function cmdFillForm({ fields, submit_selector, tab_id, frame_id }) {
|
|
|
1809
1833
|
|
|
1810
1834
|
// --- viewport_resize ---
|
|
1811
1835
|
|
|
1812
|
-
async function cmdViewportResize({ preset, width, height, read_only = false, tab_id }) {
|
|
1836
|
+
async function cmdViewportResize({ preset, width, height, left, top, state, read_only = false, tab_id }) {
|
|
1813
1837
|
const tabId = await resolveTabId(tab_id);
|
|
1814
1838
|
const tab = await chrome.tabs.get(tabId);
|
|
1815
1839
|
|
|
@@ -1825,9 +1849,20 @@ async function cmdViewportResize({ preset, width, height, read_only = false, tab
|
|
|
1825
1849
|
const updateOpts = {};
|
|
1826
1850
|
if (targetW) updateOpts.width = targetW;
|
|
1827
1851
|
if (targetH) updateOpts.height = targetH;
|
|
1852
|
+
if (typeof left === 'number') updateOpts.left = left;
|
|
1853
|
+
if (typeof top === 'number') updateOpts.top = top;
|
|
1828
1854
|
|
|
1829
1855
|
if (!read_only) {
|
|
1830
|
-
|
|
1856
|
+
// Lo stato va cambiato PRIMA dei bounds: una finestra massimizzata li
|
|
1857
|
+
// accetta e li ignora, e la chiamata riuscirebbe senza spostare nulla.
|
|
1858
|
+
if (state) await chrome.windows.update(tab.windowId, { state });
|
|
1859
|
+
if (Object.keys(updateOpts).length === 0) {
|
|
1860
|
+
if (state) {
|
|
1861
|
+
const w = await chrome.windows.get(tab.windowId);
|
|
1862
|
+
return { requested: { state }, actual: { left: w.left, top: w.top, width: w.width, height: w.height, state: w.state } };
|
|
1863
|
+
}
|
|
1864
|
+
throw new Error('Provide preset, width, height, left, top or state');
|
|
1865
|
+
}
|
|
1831
1866
|
await chrome.windows.update(tab.windowId, updateOpts);
|
|
1832
1867
|
// La finestra si ridisegna in modo asincrono: senza la pausa si leggono le
|
|
1833
1868
|
// dimensioni vecchie e si riporta un risultato falso.
|
|
@@ -1845,7 +1880,14 @@ async function cmdViewportResize({ preset, width, height, read_only = false, tab
|
|
|
1845
1880
|
});
|
|
1846
1881
|
const actual = results?.[0]?.result ?? {};
|
|
1847
1882
|
if (read_only) return { actual };
|
|
1848
|
-
|
|
1883
|
+
const win = await chrome.windows.get(tab.windowId);
|
|
1884
|
+
return {
|
|
1885
|
+
requested: { width: targetW, height: targetH, preset: preset || null, left, top, state },
|
|
1886
|
+
actual,
|
|
1887
|
+
// Il window manager può accettare e ignorare: senza il confronto una
|
|
1888
|
+
// finestra rimasta ferma passerebbe per spostata.
|
|
1889
|
+
window: { left: win.left, top: win.top, width: win.width, height: win.height, state: win.state },
|
|
1890
|
+
};
|
|
1849
1891
|
}
|
|
1850
1892
|
|
|
1851
1893
|
// --- full_page_screenshot ---
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-bridge-mcp",
|
|
3
3
|
"mcpName": "io.github.frsorrentino/chrome-bridge",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.14.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "MCP server with 62 tools for browser automation via Chrome extension. Works on ChromeOS/Crostini.",
|
|
7
7
|
"main": "server/index.js",
|
package/server/protocol.js
CHANGED
package/server/tools.js
CHANGED
|
@@ -480,10 +480,15 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
480
480
|
server.tool(
|
|
481
481
|
'get_tabs',
|
|
482
482
|
'List every open tab with id, url, title and active flag. Read-only. Use it to find a tab_id when the '
|
|
483
|
-
+ 'implicit target (last navigated tab, else the active one) is not the tab you mean.'
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
483
|
+
+ 'implicit target (last navigated tab, else the active one) is not the tab you mean. '
|
|
484
|
+
+ 'include_windows adds the windows themselves with their position, size, state and type — what you need '
|
|
485
|
+
+ 'before moving or tiling anything, and the only way to tell which monitor a window is on.',
|
|
486
|
+
{
|
|
487
|
+
include_windows: z.boolean().optional().default(false)
|
|
488
|
+
.describe('Also return the windows with bounds, state, type and tab count'),
|
|
489
|
+
},
|
|
490
|
+
async ({ include_windows }) => {
|
|
491
|
+
const data = await send(MessageType.GET_TABS, { include_windows: include_windows === true });
|
|
487
492
|
return {
|
|
488
493
|
content: [{
|
|
489
494
|
type: 'text',
|
|
@@ -1028,12 +1033,16 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1028
1033
|
{
|
|
1029
1034
|
action: z.enum(['set', 'get']).optional().default('set').describe('get reports the current viewport without resizing anything'),
|
|
1030
1035
|
preset: z.enum(['mobile', 'tablet', 'desktop']).optional().describe('375x812, 768x1024, 1440x900'),
|
|
1036
|
+
left: z.number().optional().describe('Window x on the virtual desktop; on multi-monitor this is what picks the screen'),
|
|
1037
|
+
top: z.number().optional().describe('Window y on the virtual desktop'),
|
|
1038
|
+
state: z.enum(['normal', 'maximized', 'fullscreen', 'minimized']).optional()
|
|
1039
|
+
.describe('Applied before bounds: a maximized window accepts left/top/width/height and ignores them'),
|
|
1031
1040
|
width: z.number().optional().describe('Overrides preset'),
|
|
1032
1041
|
height: z.number().optional().describe('Overrides preset'),
|
|
1033
1042
|
tab_id: tabId,
|
|
1034
1043
|
},
|
|
1035
|
-
async ({ action, preset, width, height, tab_id }) => {
|
|
1036
|
-
const data = await send(MessageType.VIEWPORT_RESIZE, { preset, width, height, read_only: (action ?? 'set') === 'get', tab_id });
|
|
1044
|
+
async ({ action, preset, width, height, left, top, state, tab_id }) => {
|
|
1045
|
+
const data = await send(MessageType.VIEWPORT_RESIZE, { preset, width, height, left, top, state, read_only: (action ?? 'set') === 'get', tab_id });
|
|
1037
1046
|
return {
|
|
1038
1047
|
content: [{
|
|
1039
1048
|
type: 'text',
|
|
@@ -1765,10 +1774,13 @@ export function registerTools(server, wsManager, caps = 'all') {
|
|
|
1765
1774
|
layout: z.enum(['grid', 'columns', 'rows']).optional().default('grid').describe('columns splits left to right, rows top to bottom, grid keeps tiles as square as it can'),
|
|
1766
1775
|
padding: z.number().optional().default(0).describe('Px of empty margin kept inside the work area'),
|
|
1767
1776
|
include_types: z.array(z.enum(['normal', 'popup', 'app'])).optional().describe('Window types to include; omitted = normal only'),
|
|
1777
|
+
area: z.object({
|
|
1778
|
+
left: z.number(), top: z.number(), width: z.number(), height: z.number(),
|
|
1779
|
+
}).optional().describe('Monitor area to fill, when no target window has a scriptable tab to read it from'),
|
|
1768
1780
|
},
|
|
1769
|
-
async ({ window_ids, reference_window_id, layout, padding, include_types }) => {
|
|
1781
|
+
async ({ window_ids, reference_window_id, layout, padding, include_types, area }) => {
|
|
1770
1782
|
const data = await send(MessageType.TILE_WINDOWS, {
|
|
1771
|
-
window_ids, reference_window_id, layout: layout ?? 'grid', padding: padding ?? 0, include_types,
|
|
1783
|
+
window_ids, reference_window_id, layout: layout ?? 'grid', padding: padding ?? 0, include_types, area,
|
|
1772
1784
|
});
|
|
1773
1785
|
return { content: [{ type: 'text', text: jsonText(data) }] };
|
|
1774
1786
|
}
|