bwb-browser 3.1.0 → 3.2.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/AGENTS.md +8 -8
- package/README.md +34 -4
- package/lib/act.mjs +26 -3
- package/lib/browser.mjs +11 -1
- package/lib/diagnose.mjs +11 -1
- package/lib/fingerprint.mjs +9 -4
- package/lib/helpers.mjs +1 -2
- package/package.json +1 -1
- package/server.mjs +73 -25
package/AGENTS.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# bwb-browser — Agent Integration Guide
|
|
2
2
|
|
|
3
3
|
> **Author:** Krish Tiwari ([@krshforever](https://github.com/krshforever))
|
|
4
|
-
> **Package:** [`bwb-browser`](https://www.npmjs.com/package/bwb-browser) · 76KB source ·
|
|
5
|
-
> **Last updated:** 2026-
|
|
4
|
+
> **Package:** [`bwb-browser`](https://www.npmjs.com/package/bwb-browser) · 76KB source · 26 tools
|
|
5
|
+
> **Last updated:** 2026-08-06
|
|
6
6
|
|
|
7
7
|
## What is bwb?
|
|
8
8
|
|
|
9
|
-
**Browser Without Bloat** — a lightweight MCP server that gives any AI agent browser superpowers. 76KB.
|
|
9
|
+
**Browser Without Bloat** — a lightweight MCP server that gives any AI agent browser superpowers. 76KB. 26 tools. Zero heavy dependencies.
|
|
10
10
|
|
|
11
11
|
While other MCP browser tools ship a full browser binary (Playwright MCP = ~250MB, Puppeteer MCP = ~400MB), bwb uses **raw Chrome DevTools Protocol (CDP)** — the protocol Chrome speaks natively. It auto-detects the browser already on your system.
|
|
12
12
|
|
|
@@ -52,7 +52,7 @@ npm install -g bwb-browser
|
|
|
52
52
|
|
|
53
53
|
# 2. Verify it works
|
|
54
54
|
bwb --version
|
|
55
|
-
# → bwb-browser 3.
|
|
55
|
+
# → bwb-browser 3.2.0
|
|
56
56
|
|
|
57
57
|
# 3. Add to your AI agent's MCP config
|
|
58
58
|
```
|
|
@@ -64,10 +64,10 @@ bwb --version
|
|
|
64
64
|
Give this to any AI agent (Claude, OpenCode, Gemini, etc.) to auto-install and configure bwb:
|
|
65
65
|
|
|
66
66
|
```
|
|
67
|
-
I want you to install and use bwb-browser — a 76KB MCP browser automation server with
|
|
67
|
+
I want you to install and use bwb-browser — a 76KB MCP browser automation server with 26 tools.
|
|
68
68
|
|
|
69
69
|
bwb is Browser Without Bloat. It uses raw Chrome DevTools Protocol (no Playwright, no Puppeteer).
|
|
70
|
-
It has
|
|
70
|
+
It has 26 tools including:
|
|
71
71
|
|
|
72
72
|
- `browser_act` — natural language interaction ("search for X", "click the button", "what's on this page")
|
|
73
73
|
- `browser_watch` — live capture of console messages, network requests, JS exceptions, page navigations
|
|
@@ -126,7 +126,7 @@ When using bwb for web tasks, follow these patterns:
|
|
|
126
126
|
### Basic Browsing
|
|
127
127
|
- `browser_goto({url: "https://example.com"})` — navigate to a page
|
|
128
128
|
- `browser_title()` — check page title
|
|
129
|
-
- `browser_screenshot()` — take a screenshot (
|
|
129
|
+
- `browser_screenshot({selector: "#chart"})` — take a screenshot (whole page, viewport, or one element; saves to /storage/emulated/0/Download/bwb-screenshots/ on Android or ~/bwb-screenshots/ on desktop)
|
|
130
130
|
- `browser_text()` — get page text content
|
|
131
131
|
- `browser_html()` — get page HTML
|
|
132
132
|
- `browser_elements({kind: "links"|"buttons"|"inputs"|"headings"})` — find interactive elements
|
|
@@ -167,7 +167,7 @@ internally — not just what it looks like.
|
|
|
167
167
|
| **`browser_diagnose`** | 🔥 Full page health check — perf, errors, broken images, score |
|
|
168
168
|
| **`browser_fingerprint`** | 🔥 Realistic browser profile for testing |
|
|
169
169
|
| `browser_goto` | Navigate to a URL |
|
|
170
|
-
| `browser_screenshot` | Take a screenshot (saves to disk + returns base64) |
|
|
170
|
+
| `browser_screenshot` | Take a screenshot — full page, viewport, or a single element via `selector` (saves to disk + returns base64) |
|
|
171
171
|
| `browser_html` | Get page/selector HTML |
|
|
172
172
|
| `browser_text` | Get page/selector visible text |
|
|
173
173
|
| `browser_click` | Click an element (native CDP mouse events) |
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# bwb-browser
|
|
2
2
|
|
|
3
|
-
**Browser Without Bloat** — 76KB.
|
|
3
|
+
**Browser Without Bloat** — 76KB. 26 tools. Zero dependencies. Runs on your phone.
|
|
4
4
|
|
|
5
5
|
A lightweight MCP server that gives any AI agent browser superpowers. Written by a guy in India on Termux because the existing tools were 200MB of "why."
|
|
6
6
|
|
|
@@ -74,6 +74,36 @@ Create tabs, close them, switch between them, save cookies, load them back. Like
|
|
|
74
74
|
|
|
75
75
|
Normalizes `navigator.webdriver`, plugins, languages, and user-agent for testing environments. Not "stealth mode" — just honest fingerprint normalization so your tests actually match real user conditions.
|
|
76
76
|
|
|
77
|
+
### 7. Element Screenshots (new in 3.2.0)
|
|
78
|
+
|
|
79
|
+
Capture just one element — a login form, a chart, a product card — not the whole page:
|
|
80
|
+
|
|
81
|
+
```javascript
|
|
82
|
+
browser_screenshot({selector: "#price-chart"})
|
|
83
|
+
browser_screenshot({selector: "h1"}) // The headline, cropped
|
|
84
|
+
browser_screenshot({fullPage: true}) // The whole page
|
|
85
|
+
browser_screenshot({}) // Just the viewport
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Every screenshot is saved to disk (Android: `/storage/emulated/0/Download/bwb-screenshots/`, desktop: `~/bwb-screenshots/`) **and** returned to your agent as a base64 image.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## What's New in 3.2.0
|
|
93
|
+
|
|
94
|
+
### New
|
|
95
|
+
- **`browser_screenshot({ selector })`** — element-level capture. Grab just the login form, the chart, the product card — not the whole page.
|
|
96
|
+
- **Screenshot directory auto-detect** — Termux/Android → `/storage/emulated/0/Download/bwb-screenshots/`, desktop → `~/bwb-screenshots/`. Override with `BWB_SCREENSHOTS_DIR`.
|
|
97
|
+
|
|
98
|
+
### Bug Fixes
|
|
99
|
+
- **`browser_back` rewritten** — native CDP history navigation (the `Page.goBack` call doesn't exist in bundled CDP 1.3; the old `history.back()` JS hack is gone). Verified with real two-step back navigation.
|
|
100
|
+
- **`browser_act` precision fixes** — navigation regex no longer swallows compound instructions ("go to X and read the title" works), "fill X with Y" vs "type Y in X" no longer swap target/text, and `search` can't hijack "fill search with X"
|
|
101
|
+
- **`killOrphanedChrome` safety** — graceful SIGTERM→SIGKILL, now scoped to bwb's own profile so it never kills another agent's browser
|
|
102
|
+
- **`browser_restart` hygiene** — watch listeners can't outlive the dying protocol
|
|
103
|
+
- **`browser_status` accuracy** — uses the real bound port, no more hardcoded 9222 poke
|
|
104
|
+
|
|
105
|
+
*Fixes from the PR #1 code review by @netzro (Hermes Agent) are incorporated and credited in the [changelog](./CHANGELOG.md).*
|
|
106
|
+
|
|
77
107
|
---
|
|
78
108
|
|
|
79
109
|
## Quick Install
|
|
@@ -81,7 +111,7 @@ Normalizes `navigator.webdriver`, plugins, languages, and user-agent for testing
|
|
|
81
111
|
```bash
|
|
82
112
|
npm install -g bwb-browser
|
|
83
113
|
bwb --version
|
|
84
|
-
# → bwb-browser 3.
|
|
114
|
+
# → bwb-browser 3.2.0
|
|
85
115
|
```
|
|
86
116
|
|
|
87
117
|
Done. If you have Chrome/Chromium anywhere on your system, bwb finds it. No config files. No environment variables. Just works.
|
|
@@ -97,7 +127,7 @@ bwb
|
|
|
97
127
|
|
|
98
128
|
---
|
|
99
129
|
|
|
100
|
-
## All
|
|
130
|
+
## All 26 Tools
|
|
101
131
|
|
|
102
132
|
| Tool | Description |
|
|
103
133
|
|------|-------------|
|
|
@@ -106,7 +136,7 @@ bwb
|
|
|
106
136
|
| **`browser_diagnose`** | 🔥 Full page health check — perf, errors, broken images, score |
|
|
107
137
|
| **`browser_fingerprint`** | 🔥 Realistic browser profile for testing |
|
|
108
138
|
| `browser_goto` | Navigate to a URL |
|
|
109
|
-
| `browser_screenshot` | Take a screenshot |
|
|
139
|
+
| `browser_screenshot` | Take a screenshot — whole page, viewport, or a single element via `selector` |
|
|
110
140
|
| `browser_html` | Get page/selector HTML |
|
|
111
141
|
| `browser_text` | Get page/selector text |
|
|
112
142
|
| `browser_title` | Get page title |
|
package/lib/act.mjs
CHANGED
|
@@ -165,7 +165,10 @@ export async function executeInstruction(protocol, instruction) {
|
|
|
165
165
|
const lower = instruction.trim().toLowerCase();
|
|
166
166
|
|
|
167
167
|
// ─── Pattern: "go to URL" / "navigate to URL" / "open URL" ──────────────
|
|
168
|
-
|
|
168
|
+
// Capture only the URL token — stop at natural instruction boundaries so
|
|
169
|
+
// compound instructions ("go to X and tell me the title") don't swallow
|
|
170
|
+
// the whole sentence into the URL.
|
|
171
|
+
const navMatch = lower.match(/^(?:go to|navigate to|open|visit|take me to)\s+([^\s,]+(?:\.[^\s,]+)*)(?=\s|$)/);
|
|
169
172
|
if (navMatch) {
|
|
170
173
|
const url = normalizeUrl(navMatch[1]);
|
|
171
174
|
if (url) {
|
|
@@ -187,7 +190,9 @@ export async function executeInstruction(protocol, instruction) {
|
|
|
187
190
|
// If navMatch but URL was invalid (normalizeUrl returned null), fall through to search
|
|
188
191
|
|
|
189
192
|
// ─── Pattern: "search for X" / "search X" ───────────────────────────────
|
|
190
|
-
|
|
193
|
+
// Anchored to string start: unanchored, "fill search with X" would be
|
|
194
|
+
// hijacked by this pattern before fill could handle it.
|
|
195
|
+
const searchMatch = lower.match(/^search\s+(?:for\s+)?(.+)/);
|
|
191
196
|
if (searchMatch) {
|
|
192
197
|
const query = searchMatch[1];
|
|
193
198
|
let inputInfo = await findInput(Runtime, "search");
|
|
@@ -263,7 +268,25 @@ export async function executeInstruction(protocol, instruction) {
|
|
|
263
268
|
}
|
|
264
269
|
|
|
265
270
|
// ─── Pattern: "fill X with Y" / "type Y in X" / "enter Y into X" ──────
|
|
266
|
-
|
|
271
|
+
// NOTE: "fill X with Y" puts the VALUE in X ("fill email with foo@bar.com"),
|
|
272
|
+
// while "type Y in X" puts the TEXT in X ("type hello in search").
|
|
273
|
+
const fillWithMatch = lower.match(/fill\s+(.+?)\s+with\s+(.+)/);
|
|
274
|
+
if (fillWithMatch) {
|
|
275
|
+
const target = fillWithMatch[1];
|
|
276
|
+
const text = fillWithMatch[2];
|
|
277
|
+
const inputInfo = await findInput(Runtime, target);
|
|
278
|
+
if (inputInfo.found && inputInfo.selector) {
|
|
279
|
+
await Runtime.evaluate({
|
|
280
|
+
expression: `document.querySelector(${JSON.stringify(inputInfo.selector)})?.focus()`,
|
|
281
|
+
});
|
|
282
|
+
await Input.insertText({ text });
|
|
283
|
+
await new Promise(r => setTimeout(r, 200));
|
|
284
|
+
return { action: "fill", target, text, inputTag: inputInfo.tag };
|
|
285
|
+
}
|
|
286
|
+
return { action: "fill_error", target, error: `Could not find input matching "${target}"` };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const fillMatch = lower.match(/(?:type|enter)\s+(.+?)\s+(?:in|into)\s+(.+)/);
|
|
267
290
|
if (fillMatch) {
|
|
268
291
|
const text = fillMatch[1];
|
|
269
292
|
const target = fillMatch[2];
|
package/lib/browser.mjs
CHANGED
|
@@ -41,8 +41,18 @@ export const cfg = {
|
|
|
41
41
|
// NOT the user's normal Chrome browser.
|
|
42
42
|
export function killOrphanedChrome() {
|
|
43
43
|
try {
|
|
44
|
+
// Only touch Chromes using OUR user-data-dir — never another agent's browser.
|
|
45
|
+
const scopedMatch = cfg.userDataDir
|
|
46
|
+
? `grep "remote-debugging-port" | grep "${String(cfg.userDataDir).replace(/["\\]/g, "\\$&")}" | grep -v grep`
|
|
47
|
+
: `grep "remote-debugging-port" | grep -v grep`;
|
|
48
|
+
// SIGTERM first for clean shutdown
|
|
44
49
|
execSync(
|
|
45
|
-
`ps aux |
|
|
50
|
+
`ps aux | ${scopedMatch} | awk '{print $2}' | xargs -r kill -15 2>/dev/null; true`,
|
|
51
|
+
{ encoding: "utf8", timeout: 5000 }
|
|
52
|
+
);
|
|
53
|
+
// Give them a moment to exit cleanly, then SIGKILL survivors
|
|
54
|
+
execSync(
|
|
55
|
+
`sleep 1 && ps aux | ${scopedMatch} | awk '{print $2}' | xargs -r kill -9 2>/dev/null; true`,
|
|
46
56
|
{ encoding: "utf8", timeout: 5000 }
|
|
47
57
|
);
|
|
48
58
|
} catch {}
|
package/lib/diagnose.mjs
CHANGED
|
@@ -16,9 +16,16 @@ export async function diagnosePage(protocol) {
|
|
|
16
16
|
// Collect console errors during diagnostic
|
|
17
17
|
const consoleErrors = [];
|
|
18
18
|
let unsubConsole, unsubException;
|
|
19
|
+
let runtimeWasAlreadyEnabled = false;
|
|
19
20
|
|
|
20
21
|
try {
|
|
21
|
-
|
|
22
|
+
// Check if Runtime is already enabled; if not, enable it
|
|
23
|
+
try {
|
|
24
|
+
await Runtime.enable();
|
|
25
|
+
} catch {
|
|
26
|
+
// Already enabled — that's fine, but don't disable it later
|
|
27
|
+
runtimeWasAlreadyEnabled = true;
|
|
28
|
+
}
|
|
22
29
|
unsubConsole = Runtime.consoleAPICalled((params) => {
|
|
23
30
|
if (params.type === "error" || params.type === "warning") {
|
|
24
31
|
consoleErrors.push({
|
|
@@ -114,6 +121,9 @@ export async function diagnosePage(protocol) {
|
|
|
114
121
|
// Cleanup
|
|
115
122
|
try { if (unsubConsole) unsubConsole(); } catch {}
|
|
116
123
|
try { if (unsubException) unsubException(); } catch {}
|
|
124
|
+
if (!runtimeWasAlreadyEnabled) {
|
|
125
|
+
try { await Runtime.disable(); } catch {}
|
|
126
|
+
}
|
|
117
127
|
|
|
118
128
|
return {
|
|
119
129
|
url: currentUrl,
|
package/lib/fingerprint.mjs
CHANGED
|
@@ -49,11 +49,16 @@ export async function applyRealisticProfile(protocol) {
|
|
|
49
49
|
configurable: true,
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
-
// Remove automation-specific chrome.runtime
|
|
52
|
+
// Remove automation-specific chrome.runtime (strict-mode safe:
|
|
53
|
+
// delete on a non-configurable prop throws, defineProperty with a
|
|
54
|
+
// getter is legal even in strict mode)
|
|
53
55
|
if (window.chrome) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
try {
|
|
57
|
+
Object.defineProperty(window.chrome, 'runtime', {
|
|
58
|
+
get: () => undefined,
|
|
59
|
+
configurable: true,
|
|
60
|
+
});
|
|
61
|
+
} catch {}
|
|
57
62
|
if (!window.chrome.loadTimes) {
|
|
58
63
|
window.chrome.loadTimes = function() { return {}; };
|
|
59
64
|
}
|
package/lib/helpers.mjs
CHANGED
|
@@ -116,8 +116,7 @@ export async function waitForSelector(runtime, selector, opts = {}) {
|
|
|
116
116
|
const info = JSON.parse(result?.value || "{}");
|
|
117
117
|
|
|
118
118
|
if (disappear && info.status === "NOT_FOUND") return true;
|
|
119
|
-
if (!disappear && info.status === "FOUND" && !info.hidden) return true;
|
|
120
|
-
if (!disappear && info.status === "FOUND" && !opts.visible) return true;
|
|
119
|
+
if (!disappear && info.status === "FOUND" && (!opts.visible || !info.hidden)) return true;
|
|
121
120
|
|
|
122
121
|
await new Promise((r) => setTimeout(r, 200));
|
|
123
122
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bwb-browser",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Browser Without Bloat — 76KB MCP server that gives AI agents browser superpowers. Natural language interaction, live event watching, persistent sessions, multi-tab. Runs on Termux/Android, desktop, and CI. Zero heavy dependencies.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"bwb": "bin/bwb"
|
package/server.mjs
CHANGED
|
@@ -18,8 +18,8 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
18
18
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19
19
|
import { z } from "zod";
|
|
20
20
|
import CDP from "chrome-remote-interface";
|
|
21
|
-
import { mkdirSync } from "fs";
|
|
22
|
-
import { homedir } from "os";
|
|
21
|
+
import { mkdirSync, readFileSync, existsSync } from "fs";
|
|
22
|
+
import { homedir, platform } from "os";
|
|
23
23
|
import { join, dirname } from "path";
|
|
24
24
|
import { fileURLToPath } from "url";
|
|
25
25
|
|
|
@@ -52,6 +52,11 @@ import { executeInstruction } from "./lib/act.mjs";
|
|
|
52
52
|
|
|
53
53
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
54
54
|
|
|
55
|
+
// Read version from package.json — single source of truth
|
|
56
|
+
const { version: BWB_VERSION } = JSON.parse(
|
|
57
|
+
readFileSync(join(__dirname, 'package.json'), 'utf8')
|
|
58
|
+
);
|
|
59
|
+
|
|
55
60
|
function parseArgs() {
|
|
56
61
|
const args = process.argv.slice(2);
|
|
57
62
|
const cliCfg = {};
|
|
@@ -63,7 +68,7 @@ function parseArgs() {
|
|
|
63
68
|
case "--headless": cliCfg.headless = args[++i] !== "false"; break;
|
|
64
69
|
case "--screenshots-dir": cliCfg.screenshotsDir = args[++i]; break;
|
|
65
70
|
case "--timeout": cliCfg.navTimeout = parseInt(args[++i], 10); break;
|
|
66
|
-
case "--version": console.log(
|
|
71
|
+
case "--version": console.log(`bwb-browser ${BWB_VERSION}`); process.exit(0);
|
|
67
72
|
case "--help": printHelp(); process.exit(0);
|
|
68
73
|
}
|
|
69
74
|
}
|
|
@@ -72,9 +77,9 @@ function parseArgs() {
|
|
|
72
77
|
|
|
73
78
|
function printHelp() {
|
|
74
79
|
console.log(`
|
|
75
|
-
bwb-browser
|
|
80
|
+
bwb-browser v${BWB_VERSION} — Browser Without Bloat
|
|
76
81
|
|
|
77
|
-
Browser automation for AI agents. 76KB.
|
|
82
|
+
Browser automation for AI agents. 76KB. 26 tools. Zero heavy dependencies.
|
|
78
83
|
Uses raw CDP — no Playwright, no Puppeteer, no 400MB downloads.
|
|
79
84
|
|
|
80
85
|
Built on Termux/Android. Runs everywhere. Weighs nothing.
|
|
@@ -92,7 +97,7 @@ OPTIONS:
|
|
|
92
97
|
--version Print version
|
|
93
98
|
--help Show this help
|
|
94
99
|
|
|
95
|
-
TOOLS (
|
|
100
|
+
TOOLS (26):
|
|
96
101
|
CORE BROWSING:
|
|
97
102
|
browser_goto Navigate to a URL
|
|
98
103
|
browser_screenshot Take a screenshot
|
|
@@ -169,7 +174,14 @@ Object.assign(cfg, parseArgs());
|
|
|
169
174
|
cfg.port = cfg.port || parseInt(process.env.BWB_CDP_PORT || "0", 10);
|
|
170
175
|
cfg.headless = cfg.headless !== undefined ? cfg.headless : (process.env.BWB_HEADLESS !== "false");
|
|
171
176
|
cfg.userDataDir = cfg.userDataDir || process.env.BWB_USER_DATA_DIR || join(homedir(), ".cache", "bwb-browser");
|
|
172
|
-
cfg.screenshotsDir = cfg.screenshotsDir || process.env.BWB_SCREENSHOTS_DIR ||
|
|
177
|
+
cfg.screenshotsDir = cfg.screenshotsDir || process.env.BWB_SCREENSHOTS_DIR || (() => {
|
|
178
|
+
// Auto-detect: Termux/Android path if available, else ~/bwb-screenshots/
|
|
179
|
+
const androidPath = "/storage/emulated/0/Download/bwb-screenshots";
|
|
180
|
+
if (platform() === "android" && existsSync("/storage/emulated/0/Download")) return androidPath;
|
|
181
|
+
if (process.env.HOME?.includes("com.termux")) return androidPath;
|
|
182
|
+
if (process.env.TERMUX_VERSION) return androidPath;
|
|
183
|
+
return join(homedir(), "bwb-screenshots");
|
|
184
|
+
})();
|
|
173
185
|
cfg.navTimeout = cfg.navTimeout || parseInt(process.env.BWB_NAV_TIMEOUT || "30000", 10);
|
|
174
186
|
|
|
175
187
|
try { mkdirSync(cfg.screenshotsDir, { recursive: true }); } catch {}
|
|
@@ -224,7 +236,7 @@ function setupWatch(events, cdp) {
|
|
|
224
236
|
|
|
225
237
|
// ─── MCP Server ───────────────────────────────────────────────────────────────
|
|
226
238
|
|
|
227
|
-
const server = new McpServer({ name: "bwb-browser", version:
|
|
239
|
+
const server = new McpServer({ name: "bwb-browser", version: BWB_VERSION });
|
|
228
240
|
|
|
229
241
|
// Tool implementations
|
|
230
242
|
const tools = {
|
|
@@ -243,16 +255,43 @@ const tools = {
|
|
|
243
255
|
},
|
|
244
256
|
|
|
245
257
|
browser_screenshot: {
|
|
246
|
-
description: "Take a screenshot of the current page.",
|
|
258
|
+
description: "Take a screenshot of the current page. Pass a CSS selector to capture just that element.",
|
|
247
259
|
schema: {
|
|
248
260
|
fullPage: z.boolean().describe("Full page screenshot (default false)").optional(),
|
|
249
261
|
quality: z.number().describe("JPEG quality 0-100 (default 80)").optional(),
|
|
262
|
+
selector: z.string().describe("CSS selector to capture only that element (optional)").optional(),
|
|
250
263
|
},
|
|
251
|
-
handler: async ({ fullPage = false, quality = 80 }) => {
|
|
252
|
-
const { Page } = await getActiveProtocol();
|
|
253
|
-
|
|
264
|
+
handler: async ({ fullPage = false, quality = 80, selector }) => {
|
|
265
|
+
const { Page, Runtime } = await getActiveProtocol();
|
|
266
|
+
let clip;
|
|
267
|
+
if (selector) {
|
|
268
|
+
// Element capture: compute bounding rect in page coords, then clip
|
|
269
|
+
const { result } = await Runtime.evaluate({
|
|
270
|
+
expression: `(() => {
|
|
271
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
272
|
+
if (!el) return null;
|
|
273
|
+
const r = el.getBoundingClientRect();
|
|
274
|
+
const sx = window.scrollX || document.documentElement.scrollLeft;
|
|
275
|
+
const sy = window.scrollY || document.documentElement.scrollTop;
|
|
276
|
+
return JSON.stringify({ x: r.x + sx, y: r.y + sy, width: r.width, height: r.height });
|
|
277
|
+
})()`,
|
|
278
|
+
returnByValue: true,
|
|
279
|
+
});
|
|
280
|
+
const rect = result?.value ? JSON.parse(result.value) : null;
|
|
281
|
+
if (!rect || !rect.width || !rect.height) {
|
|
282
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: `Element not found or not visible: ${selector}` }) }] };
|
|
283
|
+
}
|
|
284
|
+
clip = { ...rect, scale: 1 };
|
|
285
|
+
}
|
|
286
|
+
// captureBeyondViewport=false keeps clip math in viewport space; a 0-size clip
|
|
287
|
+
// would be rejected by CDP, so guard against degenerate rects above.
|
|
288
|
+
const { data } = await Page.captureScreenshot({
|
|
289
|
+
format: "jpeg", quality,
|
|
290
|
+
captureBeyondViewport: fullPage || !!clip,
|
|
291
|
+
...(clip ? { clip } : {}),
|
|
292
|
+
});
|
|
254
293
|
const savedPath = saveScreenshot(data);
|
|
255
|
-
const response = { screenshot: `data:image/jpeg;base64,${data.slice(0, 40)}
|
|
294
|
+
const response = { screenshot: `data:image/jpeg;base64,${data.slice(0, 40)}...`, captured: clip ? selector : (fullPage ? "full page" : "viewport") };
|
|
256
295
|
if (savedPath) response.savedTo = savedPath;
|
|
257
296
|
return { content: [
|
|
258
297
|
{ type: "image", data, mimeType: "image/jpeg" },
|
|
@@ -312,8 +351,17 @@ const tools = {
|
|
|
312
351
|
schema: {},
|
|
313
352
|
handler: async () => {
|
|
314
353
|
const { Page, Runtime } = await getActiveProtocol();
|
|
315
|
-
|
|
316
|
-
|
|
354
|
+
// Native CDP back: walk history via navigation entries.
|
|
355
|
+
// NOTE: Page.goBack doesn't exist in the bundled CDP 1.3 protocol —
|
|
356
|
+
// getNavigationHistory + navigateToHistoryEntry is the native equivalent.
|
|
357
|
+
// The wrapper returns history FLAT ({currentIndex, entries}), not {result:{...}}.
|
|
358
|
+
const hist = await Page.getNavigationHistory();
|
|
359
|
+
const entries = hist?.entries || [];
|
|
360
|
+
const currentIdx = hist?.currentIndex ?? -1;
|
|
361
|
+
if (currentIdx > 0 && entries[currentIdx - 1]) {
|
|
362
|
+
await Page.navigateToHistoryEntry({ entryId: entries[currentIdx - 1].id });
|
|
363
|
+
}
|
|
364
|
+
await new Promise(r => setTimeout(r, Math.min(cfg.navTimeout, 1000)));
|
|
317
365
|
const { result } = await Runtime.evaluate({ expression: "document.title" });
|
|
318
366
|
syncActiveTab(result?.value, undefined);
|
|
319
367
|
return { content: [{ type: "text", text: JSON.stringify({ title: result?.value || "" }) }] };
|
|
@@ -550,18 +598,17 @@ const tools = {
|
|
|
550
598
|
status.running = true;
|
|
551
599
|
status.pid = browser.pid;
|
|
552
600
|
status.tabs = listTabs();
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
status.
|
|
559
|
-
|
|
560
|
-
const targets = await CDP.List({ port: cfg.port || 9222 });
|
|
601
|
+
// actualCdpPort is the real bound port; cfg.port may be 0 (random).
|
|
602
|
+
// Never fall back to a hardcoded 9222 — that could be another tool's browser.
|
|
603
|
+
const listPort = actualCdpPort || cfg.port;
|
|
604
|
+
if (listPort) {
|
|
605
|
+
try {
|
|
606
|
+
status.actualPort = actualCdpPort || cfg.port;
|
|
607
|
+
const targets = await CDP.List({ port: listPort });
|
|
561
608
|
status.connected = true;
|
|
562
609
|
status.targets = targets.map(t => ({ type: t.type, url: t.url, title: t.title }));
|
|
563
|
-
}
|
|
564
|
-
}
|
|
610
|
+
} catch { status.connected = false; }
|
|
611
|
+
}
|
|
565
612
|
}
|
|
566
613
|
return { content: [{ type: "text", text: JSON.stringify(status) }] };
|
|
567
614
|
},
|
|
@@ -572,6 +619,7 @@ const tools = {
|
|
|
572
619
|
schema: {},
|
|
573
620
|
handler: async () => {
|
|
574
621
|
clearTabs(); // Kill stale tab connections before restart
|
|
622
|
+
cleanupWatch(); // Detach event listeners from the dying protocol before it's gone
|
|
575
623
|
const result = await restartBrowser();
|
|
576
624
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
577
625
|
},
|