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 +293 -0
- package/bin.js +269 -0
- package/mcp/package.json +14 -0
- package/mcp/src/exec-engine.js +424 -0
- package/mcp/src/index.js +372 -0
- package/mcp/src/snapshot.js +197 -0
- package/package.json +52 -0
- package/relay/package.json +1 -0
- package/relay/src/index.js +847 -0
- package/skills/browserforce/SKILL.md +123 -0
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
|
+
}
|
package/mcp/package.json
ADDED
|
@@ -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
|
+
}
|