browserforce 1.0.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 ADDED
@@ -0,0 +1,293 @@
1
+ # BrowserForce
2
+
3
+ **Give your AI agent your real Chrome browser.** Your logins, your cookies, your extensions — already there.
4
+
5
+ Other browser tools spawn a fresh Chrome — no logins, no extensions, instantly flagged by bot detectors. BrowserForce connects to **your running browser** instead. One Chrome extension, full Playwright API, everything you're already logged into.
6
+
7
+ Works with [OpenClaw](https://github.com/openclaw/openclaw), Claude, or any MCP-compatible agent.
8
+
9
+ | | OpenClaw's built-in browser | BrowserForce |
10
+ |---|---|---|
11
+ | Browser | Spawns dedicated Chrome | **Uses your Chrome** |
12
+ | Login state | Fresh — must log in every time | Already logged in |
13
+ | Extensions | None | Your existing ones |
14
+ | 2FA / Captchas | Blocked | Already passed (you did it) |
15
+ | Bot detection | Easily detected | Runs in your real profile |
16
+ | Cookies & sessions | Empty | Yours |
17
+
18
+ ## Setup
19
+
20
+ ### 1. Install
21
+
22
+ ```bash
23
+ npm install -g browserforce
24
+ ```
25
+
26
+ Or from source:
27
+
28
+ ```bash
29
+ git clone https://github.com/anthropics/browserforce.git
30
+ cd browserforce
31
+ pnpm install
32
+ ```
33
+
34
+ ### 2. Load the Chrome extension
35
+
36
+ 1. Open `chrome://extensions/` in Chrome
37
+ 2. Enable **Developer mode** (top-right toggle)
38
+ 3. Click **Load unpacked** → select the `extension/` folder
39
+ 4. Extension icon appears in your toolbar (gray = disconnected)
40
+
41
+ ### 3. Start the relay
42
+
43
+ ```bash
44
+ browserforce serve
45
+ ```
46
+
47
+ Or with pnpm (development):
48
+
49
+ ```bash
50
+ pnpm relay
51
+ ```
52
+
53
+ ```
54
+ BrowserForce
55
+ ────────────────────────────────────────
56
+ Status: http://127.0.0.1:19222/
57
+ CDP: ws://127.0.0.1:19222/cdp?token=<TOKEN>
58
+ ────────────────────────────────────────
59
+ ```
60
+
61
+ Extension icon turns green — you're connected.
62
+
63
+ ## Connect Your Agent
64
+
65
+ ### OpenClaw
66
+
67
+ Add to `~/.openclaw/openclaw.json`:
68
+
69
+ ```json
70
+ {
71
+ "plugins": {
72
+ "entries": {
73
+ "mcp-adapter": {
74
+ "enabled": true,
75
+ "config": {
76
+ "servers": [
77
+ {
78
+ "name": "browserforce",
79
+ "transport": "stdio",
80
+ "command": "node",
81
+ "args": ["/absolute/path/to/browserforce/mcp/src/index.js"]
82
+ }
83
+ ]
84
+ }
85
+ }
86
+ }
87
+ }
88
+ }
89
+ ```
90
+
91
+ Then add `"mcp-adapter"` to your agent's allowed tools. Your OpenClaw agent can now browse the web as you — no login flows, no captchas.
92
+
93
+ ### Claude Desktop
94
+
95
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
96
+
97
+ ```json
98
+ {
99
+ "mcpServers": {
100
+ "browserforce": {
101
+ "command": "node",
102
+ "args": ["/absolute/path/to/browserforce/mcp/src/index.js"]
103
+ }
104
+ }
105
+ }
106
+ ```
107
+
108
+ ### Claude Code
109
+
110
+ Add to `~/.claude/mcp.json`:
111
+
112
+ ```json
113
+ {
114
+ "mcpServers": {
115
+ "browserforce": {
116
+ "command": "node",
117
+ "args": ["/absolute/path/to/browserforce/mcp/src/index.js"]
118
+ }
119
+ }
120
+ }
121
+ ```
122
+
123
+ ### CLI
124
+
125
+ ```bash
126
+ npm install -g browserforce # or: pnpm add -g browserforce
127
+ ```
128
+
129
+ ```bash
130
+ browserforce serve # Start the relay server
131
+ browserforce status # Check relay and extension status
132
+ browserforce tabs # List open browser tabs
133
+ browserforce snapshot [n] # Accessibility tree of tab n
134
+ browserforce screenshot [n] # Screenshot tab n (PNG to stdout)
135
+ browserforce navigate <url> # Open URL in a new tab
136
+ browserforce -e "<code>" # Run Playwright JavaScript (one-shot)
137
+ ```
138
+
139
+ Each `-e` command is one-shot — state does not persist between calls. For persistent state, use the MCP server.
140
+
141
+ ### OpenClaw Skill
142
+
143
+ Install the skill directly:
144
+
145
+ ```bash
146
+ npx -y skills add anthropics/browserforce
147
+ ```
148
+
149
+ Or add to your agent config manually — the skill teaches the agent to use BrowserForce CLI commands via Bash.
150
+
151
+ ### Any Playwright Script
152
+
153
+ ```javascript
154
+ const { chromium } = require('playwright');
155
+
156
+ const browser = await chromium.connectOverCDP(
157
+ 'ws://127.0.0.1:19222/cdp?token=<TOKEN>'
158
+ );
159
+
160
+ const pages = browser.contexts()[0].pages();
161
+ for (const page of pages) {
162
+ console.log(page.url()); // your real tabs!
163
+ }
164
+
165
+ // Gmail is already logged in
166
+ const gmail = pages.find(p => p.url().includes('mail.google'));
167
+ await gmail.screenshot({ path: 'gmail.png' });
168
+ ```
169
+
170
+ No token config needed for MCP — the server reads it automatically from `~/.browserforce/cdp-url`.
171
+
172
+ ## What Your Agent Can Do
173
+
174
+ Once connected, your agent has full Playwright access to your real browser:
175
+
176
+ ```javascript
177
+ // Navigate (uses your cookies — no login needed)
178
+ await page.goto('https://github.com');
179
+ await waitForPageLoad();
180
+
181
+ // Read pages with accessibility snapshots (10-100x cheaper than screenshots)
182
+ return await snapshot();
183
+
184
+ // Click, type, fill forms
185
+ await page.locator('role=button[name="Sign in"]').click();
186
+ await page.locator('role=textbox[name="Search"]').fill('query');
187
+
188
+ // Screenshots when you need them
189
+ return await page.screenshot();
190
+
191
+ // Work with multiple tabs
192
+ const pages = context.pages();
193
+ const gmail = pages.find(p => p.url().includes('mail.google'));
194
+
195
+ // Persist data across calls
196
+ state.results = await page.evaluate(() => document.title);
197
+ ```
198
+
199
+ ### MCP Tools
200
+
201
+ | Tool | Description |
202
+ |------|-------------|
203
+ | `execute` | Run Playwright JavaScript in your real Chrome. Access `page`, `context`, `state`, `snapshot()`, `waitForPageLoad()`, `getLogs()`, and Node.js globals. |
204
+ | `reset` | Reconnect to the relay and clear state. Use when the connection drops. |
205
+
206
+ ## How It Works
207
+
208
+ ```
209
+ Agent (OpenClaw, Claude, etc.)
210
+
211
+ ├─ MCP server (stdio)
212
+ ├─ CLI (browserforce -e)
213
+
214
+ │ CDP over WebSocket
215
+
216
+ Relay Server (localhost:19222)
217
+
218
+ │ WebSocket
219
+
220
+ Chrome Extension (MV3)
221
+
222
+ │ chrome.debugger API
223
+
224
+ Your Real Chrome Browser
225
+ ```
226
+
227
+ The **relay server** runs on your machine (localhost only). It translates between the agent's CDP commands and the extension's debugger bridge.
228
+
229
+ The **Chrome extension** lives in your browser. It attaches Chrome's built-in debugger to your tabs and forwards commands — exactly like DevTools does.
230
+
231
+ When the agent connects, it immediately sees all your open tabs as controllable Playwright pages. No clicking, no manual attachment.
232
+
233
+ ## Extension Settings
234
+
235
+ Click the extension icon to configure:
236
+
237
+ - **Auto / Manual mode** — Let the agent create tabs freely, or manually select which tabs it can access
238
+ - **Lock URL** — Prevent the agent from navigating away from the current page
239
+ - **No new tabs** — Block tab creation
240
+ - **Read-only** — Observe only, no interactions
241
+ - **Auto-cleanup** — Automatically detach or close agent tabs after a timeout
242
+ - **Custom instructions** — Pass text instructions to the agent
243
+
244
+ ## Security
245
+
246
+ | Layer | Control |
247
+ |-------|---------|
248
+ | **Network** | Relay binds to `127.0.0.1` only — never exposed to the internet |
249
+ | **Auth** | Random token required for every CDP connection |
250
+ | **Origin** | Extension only accepts connections from its own Chrome origin |
251
+ | **Visibility** | Chrome shows "controlled by automated test software" on active tabs |
252
+
253
+ Everything runs on your machine. The auth token is stored at `~/.browserforce/auth-token` with owner-only permissions.
254
+
255
+ ## Configuration
256
+
257
+ **Custom relay port:**
258
+ ```bash
259
+ RELAY_PORT=19333 browserforce serve
260
+ ```
261
+
262
+ **Extension relay URL:** Click the extension icon → change the URL → Save. Default: `ws://127.0.0.1:19222/extension`
263
+
264
+ **Override CDP URL for MCP:**
265
+ ```json
266
+ {
267
+ "env": {
268
+ "BF_CDP_URL": "ws://127.0.0.1:19333/cdp?token=your-token"
269
+ }
270
+ }
271
+ ```
272
+
273
+ ## API
274
+
275
+ | Endpoint | Description |
276
+ |----------|-------------|
277
+ | `GET /` | Health check (extension status, target count) |
278
+ | `GET /json/version` | CDP discovery |
279
+ | `GET /json/list` | List attached targets |
280
+ | `ws://.../extension` | Chrome extension WebSocket |
281
+ | `ws://.../cdp?token=...` | Agent CDP connection |
282
+
283
+ ## Troubleshooting
284
+
285
+ | Problem | Fix |
286
+ |---------|-----|
287
+ | Extension stays gray | Is the relay running? Check `http://127.0.0.1:19222/` |
288
+ | "Another debugger attached" | Close DevTools for that tab |
289
+ | Agent sees 0 pages | Open at least one regular webpage (not `chrome://`) |
290
+ | Extension keeps reconnecting | Normal — MV3 kills idle workers; it auto-recovers |
291
+ | Port in use | `lsof -ti:19222 \| xargs kill -9` |
292
+
293
+ > **Want the full walkthrough?** Read the [User Guide](GUIDE.md) for a plain-English explanation of what this does and how to get started.
package/bin.js ADDED
@@ -0,0 +1,269 @@
1
+ #!/usr/bin/env node
2
+ // BrowserForce CLI
3
+
4
+ import { parseArgs } from 'node:util';
5
+ import http from 'node:http';
6
+
7
+ const { values, positionals } = parseArgs({
8
+ options: {
9
+ eval: { type: 'string', short: 'e' },
10
+ timeout: { type: 'string', default: '30000' },
11
+ json: { type: 'boolean', default: false },
12
+ help: { type: 'boolean', short: 'h', default: false },
13
+ },
14
+ allowPositionals: true,
15
+ strict: false,
16
+ });
17
+
18
+ const command = positionals[0] || (values.eval ? 'execute' : 'help');
19
+
20
+ // ─── Helpers ────────────────────────────────────────────────────────────────
21
+
22
+ function output(data, json) {
23
+ if (json) {
24
+ console.log(JSON.stringify(data, null, 2));
25
+ } else if (typeof data === 'string') {
26
+ console.log(data);
27
+ } else {
28
+ console.log(JSON.stringify(data, null, 2));
29
+ }
30
+ }
31
+
32
+ function httpGet(url) {
33
+ return new Promise((resolve, reject) => {
34
+ http.get(url, (res) => {
35
+ let body = '';
36
+ res.on('data', (d) => (body += d));
37
+ res.on('end', () => {
38
+ try { resolve(JSON.parse(body)); }
39
+ catch { resolve(body); }
40
+ });
41
+ }).on('error', reject);
42
+ });
43
+ }
44
+
45
+ async function connectBrowser() {
46
+ const { getCdpUrl } = await import('./mcp/src/exec-engine.js');
47
+ // playwright-core lives in mcp/node_modules (pnpm workspace sub-package).
48
+ // Use createRequire from the mcp package context to locate it, then dynamic-import.
49
+ const { createRequire } = await import('node:module');
50
+ const mReq = createRequire(new URL('./mcp/src/exec-engine.js', import.meta.url).pathname);
51
+ const pwPath = mReq.resolve('playwright-core');
52
+ const { default: pw } = await import(pwPath);
53
+ const { chromium } = pw;
54
+ const cdpUrl = getCdpUrl();
55
+ return chromium.connectOverCDP(cdpUrl);
56
+ }
57
+
58
+ function getFirstContext(browser) {
59
+ const contexts = browser.contexts();
60
+ if (contexts.length === 0) {
61
+ throw new Error('No browser context available. Is the extension connected?');
62
+ }
63
+ return contexts[0];
64
+ }
65
+
66
+ // ─── Commands ───────────────────────────────────────────────────────────────
67
+
68
+ async function cmdStatus() {
69
+ const { getRelayHttpUrl } = await import('./mcp/src/exec-engine.js');
70
+ let baseUrl;
71
+ try {
72
+ baseUrl = getRelayHttpUrl();
73
+ } catch {
74
+ baseUrl = 'http://127.0.0.1:19222';
75
+ }
76
+ try {
77
+ const data = await httpGet(`${baseUrl}/`);
78
+ output({
79
+ relay: 'running',
80
+ extension: data.extension ? 'connected' : 'disconnected',
81
+ targets: data.targets || 0,
82
+ clients: data.clients || 0,
83
+ }, values.json);
84
+ } catch {
85
+ output({ relay: 'not running' }, values.json);
86
+ process.exit(1);
87
+ }
88
+ }
89
+
90
+ async function cmdTabs() {
91
+ const browser = await connectBrowser();
92
+ try {
93
+ const ctx = getFirstContext(browser);
94
+ const pages = ctx.pages();
95
+ const tabs = pages.map((p, i) => ({ index: i, title: '', url: p.url() }));
96
+ await Promise.all(tabs.map(async (t, i) => {
97
+ try { t.title = await pages[i].title(); } catch { t.title = '(untitled)'; }
98
+ }));
99
+ if (values.json) {
100
+ output(tabs, true);
101
+ } else if (tabs.length === 0) {
102
+ console.log('No tabs available');
103
+ } else {
104
+ for (const t of tabs) {
105
+ console.log(` [${t.index}] ${t.title}`);
106
+ console.log(` ${t.url}`);
107
+ }
108
+ }
109
+ } finally {
110
+ await browser.close().catch(() => {});
111
+ }
112
+ }
113
+
114
+ async function cmdScreenshot() {
115
+ const index = parseInt(positionals[1] || '0', 10);
116
+ const browser = await connectBrowser();
117
+ try {
118
+ const pages = getFirstContext(browser).pages();
119
+ if (index >= pages.length) {
120
+ console.error(`Tab ${index} not found. ${pages.length} tab(s) available.`);
121
+ process.exit(1);
122
+ }
123
+ const buf = await pages[index].screenshot();
124
+ if (values.json) {
125
+ output({ type: 'image', data: buf.toString('base64'), mimeType: 'image/png' }, true);
126
+ } else {
127
+ process.stdout.write(buf);
128
+ }
129
+ } finally {
130
+ await browser.close().catch(() => {});
131
+ }
132
+ }
133
+
134
+ async function cmdSnapshot() {
135
+ const index = parseInt(positionals[1] || '0', 10);
136
+ const { getAccessibilityTree, getStableIds } = await import('./mcp/src/exec-engine.js');
137
+ const { buildSnapshotText, annotateStableAttrs } = await import('./mcp/src/snapshot.js');
138
+ const browser = await connectBrowser();
139
+ try {
140
+ const pages = getFirstContext(browser).pages();
141
+ if (index >= pages.length) {
142
+ console.error(`Tab ${index} not found. ${pages.length} tab(s) available.`);
143
+ process.exit(1);
144
+ }
145
+ const page = pages[index];
146
+ const axRoot = await getAccessibilityTree(page);
147
+ if (!axRoot) { console.log('No accessibility tree available.'); return; }
148
+ const stableIds = await getStableIds(page);
149
+ annotateStableAttrs(axRoot, stableIds);
150
+ const { text, refs } = buildSnapshotText(axRoot);
151
+ const refTable = refs.length > 0
152
+ ? '\n\n--- Ref → Locator ---\n' + refs.map(r => `${r.ref}: ${r.locator}`).join('\n')
153
+ : '';
154
+ const title = await page.title().catch(() => '');
155
+ output(`Page: ${title} (${page.url()})\nRefs: ${refs.length} interactive elements\n\n${text}${refTable}`, values.json);
156
+ } finally {
157
+ await browser.close().catch(() => {});
158
+ }
159
+ }
160
+
161
+ async function cmdNavigate() {
162
+ const url = positionals[1];
163
+ if (!url) { console.error('Usage: browserforce navigate <url>'); process.exit(1); }
164
+ const { smartWaitForPageLoad } = await import('./mcp/src/exec-engine.js');
165
+ const browser = await connectBrowser();
166
+ try {
167
+ const ctx = getFirstContext(browser);
168
+ const page = await ctx.newPage();
169
+ await page.goto(url);
170
+ await smartWaitForPageLoad(page, 30000);
171
+ const title = await page.title().catch(() => '');
172
+ output({ url: page.url(), title }, values.json);
173
+ } finally {
174
+ await browser.close().catch(() => {});
175
+ }
176
+ }
177
+
178
+ async function cmdExecute() {
179
+ const code = values.eval;
180
+ if (!code) { console.error('Usage: browserforce -e "<playwright code>"'); process.exit(1); }
181
+ const timeoutMs = parseInt(values.timeout, 10);
182
+ const { buildExecContext, runCode, formatResult } = await import('./mcp/src/exec-engine.js');
183
+ const browser = await connectBrowser();
184
+ try {
185
+ const ctx = getFirstContext(browser);
186
+ const pages = ctx.pages();
187
+ const page = pages[0] || null;
188
+ // One-shot state: fresh per invocation, not persistent across CLI calls
189
+ const userState = {};
190
+ const execCtx = buildExecContext(page, ctx, userState);
191
+ const result = await runCode(code, execCtx, timeoutMs);
192
+ const formatted = formatResult(result);
193
+ if (formatted.type === 'image') {
194
+ if (values.json) { output(formatted, true); }
195
+ else { process.stdout.write(Buffer.from(formatted.data, 'base64')); }
196
+ } else {
197
+ output(formatted.text, values.json);
198
+ }
199
+ } finally {
200
+ await browser.close().catch(() => {});
201
+ }
202
+ }
203
+
204
+ async function cmdServe() {
205
+ const { RelayServer } = await import('./relay/src/index.js');
206
+ const port = parseInt(process.env.RELAY_PORT || positionals[1] || '19222', 10);
207
+ const relay = new RelayServer(port);
208
+ relay.start({ writeCdpUrl: true });
209
+ process.on('SIGINT', () => { relay.stop(); process.exit(0); });
210
+ process.on('SIGTERM', () => { relay.stop(); process.exit(0); });
211
+ }
212
+
213
+ async function cmdMcp() {
214
+ await import('./mcp/src/index.js');
215
+ }
216
+
217
+ function cmdHelp() {
218
+ console.log(`
219
+ BrowserForce — Give AI agents your real Chrome browser
220
+
221
+ Usage:
222
+ browserforce serve Start the relay server
223
+ browserforce mcp Start the MCP server (stdio)
224
+ browserforce status Check relay and extension status
225
+ browserforce tabs List open browser tabs
226
+ browserforce screenshot [n] Screenshot tab n (default: 0)
227
+ browserforce snapshot [n] Accessibility tree of tab n (default: 0)
228
+ browserforce navigate <url> Open URL in a new tab
229
+ browserforce -e "<code>" Execute Playwright JavaScript (one-shot)
230
+
231
+ Options:
232
+ --timeout <ms> Execution timeout (default: 30000)
233
+ --json JSON output
234
+ -h, --help Show this help
235
+
236
+ Examples:
237
+ browserforce serve
238
+ browserforce tabs
239
+ browserforce -e "return await snapshot()"
240
+ browserforce -e "await page.goto('https://github.com'); return await snapshot()"
241
+ browserforce screenshot 0 > page.png
242
+ browserforce navigate https://gmail.com
243
+
244
+ Note: -e commands are one-shot. State does not persist between calls.
245
+ For persistent state, use the MCP server (browserforce mcp).
246
+ `);
247
+ }
248
+
249
+ // ─── Dispatch ───────────────────────────────────────────────────────────────
250
+
251
+ const commands = {
252
+ serve: cmdServe, mcp: cmdMcp, status: cmdStatus, tabs: cmdTabs,
253
+ screenshot: cmdScreenshot, snapshot: cmdSnapshot, navigate: cmdNavigate,
254
+ execute: cmdExecute, help: cmdHelp,
255
+ };
256
+
257
+ const handler = commands[command];
258
+ if (!handler) {
259
+ console.error(`Unknown command: ${command}`);
260
+ cmdHelp();
261
+ process.exit(1);
262
+ }
263
+
264
+ try {
265
+ await handler();
266
+ } catch (err) {
267
+ console.error(`Error: ${err.message}`);
268
+ process.exit(1);
269
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "browserforce-mcp",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "MCP server exposing Chrome browser control via BrowserForce",
7
+ "main": "src/index.js",
8
+ "dependencies": {
9
+ "@modelcontextprotocol/sdk": "^1.12.1",
10
+ "diff": "^8.0.3",
11
+ "playwright-core": "^1.52.0",
12
+ "zod": "^3.24.0"
13
+ }
14
+ }