@bubstack/moe-glass 0.1.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 +29 -0
- package/agents/browser-user.md +105 -0
- package/dist/LICENSE +25 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22517 -0
- package/dist/index.js.map +1 -0
- package/dist/payload.d.ts +214 -0
- package/dist/payload.d.ts.map +1 -0
- package/dist/payload.js +325 -0
- package/dist/payload.js.map +1 -0
- package/package.json +59 -0
- package/skills/browsing/COMMANDLINE-USAGE.md +595 -0
- package/skills/browsing/EXAMPLES.md +717 -0
- package/skills/browsing/README.md +55 -0
- package/skills/browsing/SKILL.md +478 -0
- package/skills/browsing/chrome-ws +1021 -0
- package/skills/browsing/chrome-ws-lib.js +461 -0
- package/skills/browsing/host-override.js +98 -0
- package/skills/browsing/lib/browser-bridge.js +175 -0
- package/skills/browsing/lib/browser-session.js +137 -0
- package/skills/browsing/lib/capture.js +499 -0
- package/skills/browsing/lib/cdp-router.js +72 -0
- package/skills/browsing/lib/cdp-utils.js +18 -0
- package/skills/browsing/lib/chrome-launcher-helpers.js +374 -0
- package/skills/browsing/lib/chrome-process.js +464 -0
- package/skills/browsing/lib/console-logging.js +70 -0
- package/skills/browsing/lib/cookies.js +17 -0
- package/skills/browsing/lib/dialogs-render.js +154 -0
- package/skills/browsing/lib/dialogs-router.js +117 -0
- package/skills/browsing/lib/dialogs.js +254 -0
- package/skills/browsing/lib/element-selector.js +91 -0
- package/skills/browsing/lib/evaluation.js +85 -0
- package/skills/browsing/lib/extraction.js +55 -0
- package/skills/browsing/lib/file-upload.js +56 -0
- package/skills/browsing/lib/html-diff.js +122 -0
- package/skills/browsing/lib/key-definitions.js +149 -0
- package/skills/browsing/lib/keyboard-input.js +288 -0
- package/skills/browsing/lib/mouse.js +423 -0
- package/skills/browsing/lib/navigation.js +272 -0
- package/skills/browsing/lib/page-scripts/dom-summary.js +31 -0
- package/skills/browsing/lib/page-scripts/markdown.js +85 -0
- package/skills/browsing/lib/page-scripts/permission-shim.js +80 -0
- package/skills/browsing/lib/page-session.js +106 -0
- package/skills/browsing/lib/profile-lock.js +179 -0
- package/skills/browsing/lib/screenshot.js +171 -0
- package/skills/browsing/lib/select-option.js +99 -0
- package/skills/browsing/lib/session-state.js +66 -0
- package/skills/browsing/lib/tabs.js +144 -0
- package/skills/browsing/lib/viewport.js +103 -0
- package/skills/browsing/lib/websocket-client.js +162 -0
- package/skills/browsing/package.json +11 -0
- package/skills/browsing/test-chrome-args.js +81 -0
- package/skills/browsing/test-cookies.js +21 -0
- package/skills/browsing/test-e2e.sh +51 -0
- package/skills/browsing/test-extract.sh +17 -0
- package/skills/browsing/test-interact.sh +11 -0
- package/skills/browsing/test-navigate.sh +9 -0
- package/skills/browsing/test-raw.sh +8 -0
- package/skills/browsing/test-tabs.sh +15 -0
- package/skills/browsing/test-viewport.js +27 -0
- package/skills/browsing/test-wait.sh +9 -0
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
# Chrome Direct Access Examples (MCP Tool)
|
|
2
|
+
|
|
3
|
+
Examples using the `use_browser` MCP tool. For command-line bash examples, see [COMMANDLINE-USAGE.md](COMMANDLINE-USAGE.md).
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [Basic Operations](#basic-operations)
|
|
8
|
+
2. [Form Automation](#form-automation)
|
|
9
|
+
3. [Web Scraping](#web-scraping)
|
|
10
|
+
4. [Multi-Tab Workflows](#multi-tab-workflows)
|
|
11
|
+
5. [Dynamic Content](#dynamic-content)
|
|
12
|
+
6. [Dialogs](#dialogs) — basic-auth, JS confirm, popup-with-confirm
|
|
13
|
+
7. [Recovery](#recovery) — auto-restart, kill/restart cycle
|
|
14
|
+
8. [Multi-MCP isolation](#multi-mcp-isolation)
|
|
15
|
+
9. [Advanced Patterns](#advanced-patterns)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Basic Operations
|
|
20
|
+
|
|
21
|
+
### Extract Page Content
|
|
22
|
+
|
|
23
|
+
Navigate to a page and extract various elements:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
{action: "navigate", payload: "https://example.com"}
|
|
27
|
+
{action: "await_element", selector: "h1"}
|
|
28
|
+
|
|
29
|
+
// Get page title
|
|
30
|
+
{action: "eval", payload: "document.title"}
|
|
31
|
+
|
|
32
|
+
// Get main heading text
|
|
33
|
+
{action: "extract", payload: "text", selector: "h1"}
|
|
34
|
+
|
|
35
|
+
// Get first link URL
|
|
36
|
+
{action: "attr", selector: "a", payload: "href"}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Get All Links
|
|
40
|
+
|
|
41
|
+
Use JavaScript evaluation to get structured data:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
{action: "navigate", payload: "https://example.com"}
|
|
45
|
+
{action: "eval", payload: "Array.from(document.querySelectorAll('a')).map(a => ({ text: a.textContent.trim(), href: a.href }))"}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Extract Table Data
|
|
49
|
+
|
|
50
|
+
Convert HTML table to structured data:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
{action: "navigate", payload: "https://example.com/data"}
|
|
54
|
+
{action: "await_element", selector: "table"}
|
|
55
|
+
|
|
56
|
+
// Convert table to JSON array
|
|
57
|
+
{action: "eval", payload: "Array.from(document.querySelectorAll('table tr')).map(row => Array.from(row.cells).map(cell => cell.textContent.trim()))"}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Get Page as Markdown
|
|
61
|
+
|
|
62
|
+
Extract entire page content in markdown format:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
{action: "navigate", payload: "https://example.com"}
|
|
66
|
+
{action: "await_element", selector: "body"}
|
|
67
|
+
{action: "extract", payload: "markdown"}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Form Automation
|
|
73
|
+
|
|
74
|
+
### Simple Login
|
|
75
|
+
|
|
76
|
+
Navigate, fill credentials, and submit:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
{action: "navigate", payload: "https://app.example.com/login"}
|
|
80
|
+
{action: "await_element", selector: "input[name=email]"}
|
|
81
|
+
|
|
82
|
+
// Fill credentials
|
|
83
|
+
{action: "type", selector: "input[name=email]", payload: "user@example.com"}
|
|
84
|
+
{action: "type", selector: "input[name=password]", payload: "securepass123\n"}
|
|
85
|
+
|
|
86
|
+
// Wait for successful login
|
|
87
|
+
{action: "await_text", payload: "Dashboard"}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Note: The `\n` at the end of the password submits the form.
|
|
91
|
+
|
|
92
|
+
### Multi-Step Form
|
|
93
|
+
|
|
94
|
+
Handle forms that show steps progressively:
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
{action: "navigate", payload: "https://example.com/register"}
|
|
98
|
+
|
|
99
|
+
// Step 1: Personal information
|
|
100
|
+
{action: "type", selector: "input[name=firstName]", payload: "John"}
|
|
101
|
+
{action: "type", selector: "input[name=lastName]", payload: "Doe"}
|
|
102
|
+
{action: "type", selector: "input[name=email]", payload: "john@example.com"}
|
|
103
|
+
{action: "click", selector: "button.next"}
|
|
104
|
+
|
|
105
|
+
// Wait for step 2 to load
|
|
106
|
+
{action: "await_element", selector: "input[name=address]"}
|
|
107
|
+
|
|
108
|
+
// Step 2: Address
|
|
109
|
+
{action: "type", selector: "input[name=address]", payload: "123 Main St"}
|
|
110
|
+
{action: "select", selector: "select[name=state]", payload: "IL"}
|
|
111
|
+
{action: "type", selector: "input[name=zip]", payload: "62701"}
|
|
112
|
+
{action: "click", selector: "button.submit"}
|
|
113
|
+
|
|
114
|
+
{action: "await_text", payload: "Registration complete"}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Search with Filters
|
|
118
|
+
|
|
119
|
+
Use dropdowns and text inputs together:
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
{action: "navigate", payload: "https://library.example.com/search"}
|
|
123
|
+
{action: "await_element", selector: "form"}
|
|
124
|
+
|
|
125
|
+
// Select category dropdown
|
|
126
|
+
{action: "select", selector: "select[name=category]", payload: "books"}
|
|
127
|
+
|
|
128
|
+
// Fill search term
|
|
129
|
+
{action: "type", selector: "input[name=query]", payload: "chrome devtools"}
|
|
130
|
+
|
|
131
|
+
// Submit and count results
|
|
132
|
+
{action: "click", selector: "button[type=submit]"}
|
|
133
|
+
{action: "await_element", selector: ".results"}
|
|
134
|
+
|
|
135
|
+
// Count results
|
|
136
|
+
{action: "eval", payload: "document.querySelectorAll('.result').length"}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Web Scraping
|
|
142
|
+
|
|
143
|
+
### Article Content
|
|
144
|
+
|
|
145
|
+
Extract article metadata and content:
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
{action: "navigate", payload: "https://blog.example.com/article"}
|
|
149
|
+
{action: "await_element", selector: "article"}
|
|
150
|
+
|
|
151
|
+
// Extract metadata
|
|
152
|
+
{action: "extract", payload: "text", selector: "article h1"}
|
|
153
|
+
{action: "extract", payload: "text", selector: ".author-name"}
|
|
154
|
+
{action: "extract", payload: "text", selector: "time"}
|
|
155
|
+
{action: "extract", payload: "text", selector: "article .content"}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Product Information
|
|
159
|
+
|
|
160
|
+
Scrape product details from e-commerce site:
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
{action: "navigate", payload: "https://shop.example.com/product/123"}
|
|
164
|
+
{action: "await_element", selector: ".product-details"}
|
|
165
|
+
|
|
166
|
+
// Extract product data
|
|
167
|
+
{action: "extract", payload: "text", selector: "h1.product-name"}
|
|
168
|
+
{action: "extract", payload: "text", selector: ".price"}
|
|
169
|
+
{action: "attr", selector: ".product-image img", payload: "src"}
|
|
170
|
+
{action: "extract", payload: "text", selector: ".stock-status"}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Batch Extract Structured Data
|
|
174
|
+
|
|
175
|
+
Get multiple products at once using JavaScript:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
{action: "navigate", payload: "https://shop.example.com/category/electronics"}
|
|
179
|
+
{action: "await_element", selector: ".product-grid"}
|
|
180
|
+
|
|
181
|
+
// Extract all products as structured data
|
|
182
|
+
{action: "eval", payload: `
|
|
183
|
+
Array.from(document.querySelectorAll('.product-card')).map(card => ({
|
|
184
|
+
name: card.querySelector('.product-name').textContent,
|
|
185
|
+
price: card.querySelector('.price').textContent,
|
|
186
|
+
image: card.querySelector('img').src,
|
|
187
|
+
url: card.querySelector('a').href
|
|
188
|
+
}))
|
|
189
|
+
`}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Multi-Tab Workflows
|
|
195
|
+
|
|
196
|
+
### Email Extraction
|
|
197
|
+
|
|
198
|
+
List tabs, then switch to the correct tab and extract data:
|
|
199
|
+
|
|
200
|
+
```
|
|
201
|
+
// Find email tab
|
|
202
|
+
{action: "list_tabs"}
|
|
203
|
+
|
|
204
|
+
// Switch to tab 2 (from list_tabs output), then operate on active tab
|
|
205
|
+
{action: "switch_tab", payload: 2}
|
|
206
|
+
{action: "click", selector: "a[title*='Organization receipt']"}
|
|
207
|
+
{action: "await_element", selector: ".email-body"}
|
|
208
|
+
|
|
209
|
+
// Extract donation amount
|
|
210
|
+
{action: "extract", payload: "text", selector: ".donation-amount"}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Price Comparison
|
|
214
|
+
|
|
215
|
+
Open multiple stores and compare prices:
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
// Navigate first tab (already active)
|
|
219
|
+
{action: "navigate", payload: "https://store1.com/product"}
|
|
220
|
+
|
|
221
|
+
// Open additional tabs and navigate each
|
|
222
|
+
{action: "new_tab"}
|
|
223
|
+
{action: "navigate", payload: "https://store2.com/product"}
|
|
224
|
+
|
|
225
|
+
{action: "new_tab"}
|
|
226
|
+
{action: "navigate", payload: "https://store3.com/product"}
|
|
227
|
+
|
|
228
|
+
// Switch back to each tab and extract prices
|
|
229
|
+
{action: "switch_tab", payload: "store1.com"}
|
|
230
|
+
{action: "await_element", selector: ".price"}
|
|
231
|
+
{action: "extract", payload: "text", selector: ".price"}
|
|
232
|
+
|
|
233
|
+
{action: "switch_tab", payload: "store2.com"}
|
|
234
|
+
{action: "await_element", selector: ".price"}
|
|
235
|
+
{action: "extract", payload: "text", selector: ".price"}
|
|
236
|
+
|
|
237
|
+
{action: "switch_tab", payload: "store3.com"}
|
|
238
|
+
{action: "await_element", selector: ".price"}
|
|
239
|
+
{action: "extract", payload: "text", selector: ".price"}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Cross-Reference Between Sites
|
|
243
|
+
|
|
244
|
+
Extract data from one site and use in another:
|
|
245
|
+
|
|
246
|
+
```
|
|
247
|
+
// Get phone number from company site
|
|
248
|
+
{action: "navigate", payload: "https://company.com/contact"}
|
|
249
|
+
{action: "await_element", selector: ".phone"}
|
|
250
|
+
{action: "extract", payload: "text", selector: ".phone"}
|
|
251
|
+
|
|
252
|
+
// Store the result, then open verification site in a new tab
|
|
253
|
+
{action: "new_tab"}
|
|
254
|
+
{action: "navigate", payload: "https://lookup.com"}
|
|
255
|
+
{action: "await_element", selector: "input[name=phone]"}
|
|
256
|
+
|
|
257
|
+
// Fill with extracted phone number (new tab is already active)
|
|
258
|
+
{action: "type", selector: "input[name=phone]", payload: "<phone-from-previous-extract>"}
|
|
259
|
+
{action: "click", selector: "button.search"}
|
|
260
|
+
{action: "await_element", selector: ".results"}
|
|
261
|
+
{action: "extract", payload: "text", selector: ".verification-status"}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Dynamic Content
|
|
267
|
+
|
|
268
|
+
### Wait for AJAX to Complete
|
|
269
|
+
|
|
270
|
+
Wait for loading spinner to disappear:
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
{action: "navigate", payload: "https://app.com/dashboard"}
|
|
274
|
+
|
|
275
|
+
// Wait for spinner to disappear using custom JavaScript
|
|
276
|
+
{action: "eval", payload: `
|
|
277
|
+
new Promise(resolve => {
|
|
278
|
+
const check = () => {
|
|
279
|
+
if (!document.querySelector('.spinner')) {
|
|
280
|
+
resolve(true);
|
|
281
|
+
} else {
|
|
282
|
+
setTimeout(check, 100);
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
check();
|
|
286
|
+
})
|
|
287
|
+
`}
|
|
288
|
+
|
|
289
|
+
// Now safe to extract
|
|
290
|
+
{action: "extract", payload: "text", selector: ".dashboard-data"}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Infinite Scroll
|
|
294
|
+
|
|
295
|
+
Scroll to load more content:
|
|
296
|
+
|
|
297
|
+
```
|
|
298
|
+
{action: "navigate", payload: "https://example.com/feed"}
|
|
299
|
+
{action: "await_element", selector: ".feed-item"}
|
|
300
|
+
|
|
301
|
+
// Scroll multiple times
|
|
302
|
+
{action: "eval", payload: "window.scrollTo(0, document.body.scrollHeight)"}
|
|
303
|
+
{action: "await_element", selector: ".feed-item", timeout: 2000}
|
|
304
|
+
|
|
305
|
+
{action: "eval", payload: "window.scrollTo(0, document.body.scrollHeight)"}
|
|
306
|
+
{action: "await_element", selector: ".feed-item", timeout: 2000}
|
|
307
|
+
|
|
308
|
+
{action: "eval", payload: "window.scrollTo(0, document.body.scrollHeight)"}
|
|
309
|
+
{action: "await_element", selector: ".feed-item", timeout: 2000}
|
|
310
|
+
|
|
311
|
+
// Count loaded items
|
|
312
|
+
{action: "eval", payload: "document.querySelectorAll('.feed-item').length"}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Wait for Element to Become Enabled
|
|
316
|
+
|
|
317
|
+
Wait for button to be clickable:
|
|
318
|
+
|
|
319
|
+
```
|
|
320
|
+
{action: "click", selector: "button.start"}
|
|
321
|
+
|
|
322
|
+
// Wait for continue button to enable
|
|
323
|
+
{action: "eval", payload: `
|
|
324
|
+
new Promise(resolve => {
|
|
325
|
+
const check = () => {
|
|
326
|
+
const btn = document.querySelector('button.continue');
|
|
327
|
+
if (btn && !btn.disabled) {
|
|
328
|
+
resolve(true);
|
|
329
|
+
} else {
|
|
330
|
+
setTimeout(check, 100);
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
check();
|
|
334
|
+
})
|
|
335
|
+
`}
|
|
336
|
+
|
|
337
|
+
{action: "click", selector: "button.continue"}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## Dialogs
|
|
343
|
+
|
|
344
|
+
### HTTP basic-auth (dialog surfaces during navigate)
|
|
345
|
+
|
|
346
|
+
When the page returns 401 + `WWW-Authenticate`, Chrome stages a basic-auth
|
|
347
|
+
dialog. The bridge intercepts the `Fetch.authRequired` event, holds the
|
|
348
|
+
navigation, and surfaces a dialog refusal — `navigate` throws with the
|
|
349
|
+
dialog grammar in the message. The response text contains `basic-auth`
|
|
350
|
+
and lists the `dialog::username`/`dialog::password`/`dialog::accept`
|
|
351
|
+
selectors.
|
|
352
|
+
|
|
353
|
+
```
|
|
354
|
+
# Step 1: navigate fails with the dialog payload
|
|
355
|
+
{action: "navigate", payload: "http://localhost:8766/", timeout: 15000}
|
|
356
|
+
# (response includes "basic-auth", "dialog::username", "dialog::password")
|
|
357
|
+
|
|
358
|
+
# Step 2-4: stage credentials and submit
|
|
359
|
+
{action: "type", selector: "dialog::username", payload: "alice"}
|
|
360
|
+
{action: "type", selector: "dialog::password", payload: "secret"}
|
|
361
|
+
{action: "click", selector: "dialog::accept"}
|
|
362
|
+
|
|
363
|
+
# Step 5: the original navigation completes; the page is now loaded
|
|
364
|
+
{action: "extract", selector: "h1", payload: "text"}
|
|
365
|
+
# → "hi alice"
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### JS confirm/alert dispatched by a click
|
|
369
|
+
|
|
370
|
+
A button whose `onclick` calls `confirm()` opens a dialog as soon as the
|
|
371
|
+
click event fires. The click itself may report a CDP timeout (Chrome
|
|
372
|
+
pauses the main thread on the dialog); that's expected. The bridge has
|
|
373
|
+
already populated `state.dialogs[sid]` and any subsequent page-targeted
|
|
374
|
+
call gets refused with the dialog grammar.
|
|
375
|
+
|
|
376
|
+
```
|
|
377
|
+
{action: "navigate", payload: "<page with onclick=confirm('Proceed?')>"}
|
|
378
|
+
{action: "click", selector: "#ask"}
|
|
379
|
+
# Click times out — expected.
|
|
380
|
+
|
|
381
|
+
{action: "extract", selector: "#result", payload: "text"}
|
|
382
|
+
# Refused: response contains "Page is behind a dialog", "dialog::accept",
|
|
383
|
+
# "dialog::dismiss", and the prompt "Proceed?".
|
|
384
|
+
|
|
385
|
+
{action: "click", selector: "dialog::accept"}
|
|
386
|
+
# Dialog accepted; state.dialogs cleared eagerly.
|
|
387
|
+
|
|
388
|
+
{action: "eval", payload: "window.__userChoice"}
|
|
389
|
+
# → true
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### Popup with synchronous dialog (Phase F headline case)
|
|
393
|
+
|
|
394
|
+
A page that opens a popup whose first inline script calls
|
|
395
|
+
`confirm()` works without races. The bridge attaches to the popup
|
|
396
|
+
target via `Target.setAutoAttach({waitForDebuggerOnStart: true})`,
|
|
397
|
+
installs the dialog shim, then resumes execution — so the synchronous
|
|
398
|
+
confirm is observed.
|
|
399
|
+
|
|
400
|
+
```
|
|
401
|
+
{action: "navigate", payload: "http://localhost:8765/popup-opener.html"}
|
|
402
|
+
{action: "click", selector: "#open"} # opens window.open('popup.html')
|
|
403
|
+
{action: "list_tabs"} # popup is enumerated
|
|
404
|
+
{action: "switch_tab", payload: "Popup"} # route to the popup tab
|
|
405
|
+
{action: "extract", selector: "*", payload: "text"}
|
|
406
|
+
# Refused with dialog grammar — the popup's confirm was caught.
|
|
407
|
+
|
|
408
|
+
{action: "click", selector: "dialog::accept"}
|
|
409
|
+
{action: "eval", payload: "window.__userChoice"}
|
|
410
|
+
# → true
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
## Recovery
|
|
414
|
+
|
|
415
|
+
### Chrome killed externally
|
|
416
|
+
|
|
417
|
+
If something kills your Chrome (`kill -9 <pid>`, OOM killer, a user
|
|
418
|
+
closing the headed window), the bridge auto-restarts on the next page
|
|
419
|
+
action. The response is prefixed with a banner so you know the previous
|
|
420
|
+
URL/tab state is gone.
|
|
421
|
+
|
|
422
|
+
```
|
|
423
|
+
{action: "navigate", payload: "https://example.com"}
|
|
424
|
+
{action: "extract", selector: "h1", payload: "text"} # → "Example Domain"
|
|
425
|
+
{action: "browser_mode"} # records pid=N
|
|
426
|
+
|
|
427
|
+
# (from your shell or another process: kill -9 N)
|
|
428
|
+
|
|
429
|
+
{action: "navigate", payload: "https://example.com"}
|
|
430
|
+
# Response starts with:
|
|
431
|
+
# [Chrome auto-restarted; URL reset to about:blank. Re-navigate to continue.]
|
|
432
|
+
# Navigated to https://example.com
|
|
433
|
+
# ...
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
`browser_mode` also reports the real PID even when the bridge adopted a
|
|
437
|
+
Chrome it didn't spawn (a leftover from a previous MCP session). So
|
|
438
|
+
"get pid, kill -9 it, watch the restart" works regardless of how Chrome
|
|
439
|
+
got there.
|
|
440
|
+
|
|
441
|
+
### Explicit kill + restart cycle
|
|
442
|
+
|
|
443
|
+
```
|
|
444
|
+
{action: "kill_chrome"} # Chrome killed.
|
|
445
|
+
{action: "restart_chrome"} # Chrome restarted in headless mode.
|
|
446
|
+
{action: "navigate", payload: "data:text/html,<h1>fresh</h1>"}
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
## Multi-MCP isolation
|
|
450
|
+
|
|
451
|
+
By default the bridge handles parallel MCP servers on the same host
|
|
452
|
+
automatically: the first claims `moe-glass:9222`, the next
|
|
453
|
+
silently falls through to `moe-glass-2:9223`, then `-3:9224`,
|
|
454
|
+
etc. Each MCP drives its own Chrome with its own profile directory.
|
|
455
|
+
|
|
456
|
+
To intentionally **share** a Chrome between processes (e.g., a
|
|
457
|
+
long-lived `chrome-ws start` from the shell + a Claude MCP attaching to
|
|
458
|
+
it), pick a fixed profile name on both sides:
|
|
459
|
+
|
|
460
|
+
```
|
|
461
|
+
# Shell:
|
|
462
|
+
CHROME_WS_PROFILE=shared chrome-ws start
|
|
463
|
+
|
|
464
|
+
# In the MCP, on first call:
|
|
465
|
+
{action: "set_profile", payload: "shared"}
|
|
466
|
+
{action: "navigate", payload: "https://example.com"}
|
|
467
|
+
# Reconnects to the shell-started Chrome — same tabs, same cookies.
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Either set `CHROME_WS_PROFILE=shared` in the MCP's environment, or call
|
|
471
|
+
`set_profile` at runtime. Both mark the profile as explicit, so the
|
|
472
|
+
bridge shares rather than disambiguates.
|
|
473
|
+
|
|
474
|
+
---
|
|
475
|
+
|
|
476
|
+
## Advanced Patterns
|
|
477
|
+
|
|
478
|
+
### Multi-Step Workflow
|
|
479
|
+
|
|
480
|
+
Complete booking flow with validation:
|
|
481
|
+
|
|
482
|
+
```
|
|
483
|
+
{action: "navigate", payload: "https://booking.example.com"}
|
|
484
|
+
|
|
485
|
+
// Search
|
|
486
|
+
{action: "type", selector: "input[name=destination]", payload: "San Francisco"}
|
|
487
|
+
{action: "type", selector: "input[name=checkin]", payload: "2025-12-01"}
|
|
488
|
+
{action: "click", selector: "button.search"}
|
|
489
|
+
|
|
490
|
+
// Select hotel
|
|
491
|
+
{action: "await_element", selector: ".hotel-results"}
|
|
492
|
+
{action: "click", selector: ".hotel-card:first-child .select"}
|
|
493
|
+
|
|
494
|
+
// Choose room
|
|
495
|
+
{action: "await_element", selector: ".room-options"}
|
|
496
|
+
{action: "click", selector: ".room[data-type=deluxe] .book"}
|
|
497
|
+
|
|
498
|
+
// Fill guest info
|
|
499
|
+
{action: "await_element", selector: "form.guest-info"}
|
|
500
|
+
{action: "type", selector: "input[name=firstName]", payload: "Jane"}
|
|
501
|
+
{action: "type", selector: "input[name=lastName]", payload: "Smith"}
|
|
502
|
+
{action: "type", selector: "input[name=email]", payload: "jane@example.com"}
|
|
503
|
+
|
|
504
|
+
// Review (don't complete)
|
|
505
|
+
{action: "click", selector: "button.review"}
|
|
506
|
+
{action: "await_element", selector: ".summary"}
|
|
507
|
+
|
|
508
|
+
// Extract confirmation details
|
|
509
|
+
{action: "extract", payload: "text", selector: ".hotel-name"}
|
|
510
|
+
{action: "extract", payload: "text", selector: ".total-price"}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### Cookies and LocalStorage
|
|
514
|
+
|
|
515
|
+
Access browser storage:
|
|
516
|
+
|
|
517
|
+
```
|
|
518
|
+
// Get cookies
|
|
519
|
+
{action: "eval", payload: "document.cookie"}
|
|
520
|
+
|
|
521
|
+
// Set cookie
|
|
522
|
+
{action: "eval", payload: "document.cookie = 'theme=dark; path=/'"}
|
|
523
|
+
|
|
524
|
+
// Get localStorage
|
|
525
|
+
{action: "eval", payload: "JSON.stringify(localStorage)"}
|
|
526
|
+
|
|
527
|
+
// Set localStorage
|
|
528
|
+
{action: "eval", payload: "localStorage.setItem('lastVisit', new Date().toISOString())"}
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
### Handle Modals
|
|
532
|
+
|
|
533
|
+
Interact with modal dialogs:
|
|
534
|
+
|
|
535
|
+
```
|
|
536
|
+
{action: "click", selector: "button.open-modal"}
|
|
537
|
+
{action: "await_element", selector: ".modal.visible"}
|
|
538
|
+
|
|
539
|
+
// Fill modal form
|
|
540
|
+
{action: "type", selector: ".modal input[name=username]", payload: "testuser"}
|
|
541
|
+
{action: "click", selector: ".modal button.submit"}
|
|
542
|
+
|
|
543
|
+
// Wait for modal to close
|
|
544
|
+
{action: "eval", payload: `
|
|
545
|
+
new Promise(resolve => {
|
|
546
|
+
const check = () => {
|
|
547
|
+
if (!document.querySelector('.modal.visible')) {
|
|
548
|
+
resolve(true);
|
|
549
|
+
} else {
|
|
550
|
+
setTimeout(check, 100);
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
check();
|
|
554
|
+
})
|
|
555
|
+
`}
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### Screenshots
|
|
559
|
+
|
|
560
|
+
Capture full page or specific elements:
|
|
561
|
+
|
|
562
|
+
```
|
|
563
|
+
// Full page screenshot
|
|
564
|
+
{action: "navigate", payload: "https://example.com"}
|
|
565
|
+
{action: "await_element", selector: "body"}
|
|
566
|
+
{action: "screenshot", payload: "/tmp/page.png"}
|
|
567
|
+
|
|
568
|
+
// Element-specific screenshot
|
|
569
|
+
{action: "screenshot", payload: "/tmp/element.png", selector: ".important-section"}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
### Check Element State
|
|
573
|
+
|
|
574
|
+
Verify element properties before interaction:
|
|
575
|
+
|
|
576
|
+
```
|
|
577
|
+
// Check if button is disabled
|
|
578
|
+
{action: "eval", payload: "document.querySelector('button.submit').disabled"}
|
|
579
|
+
|
|
580
|
+
// Check if element is visible
|
|
581
|
+
{action: "eval", payload: "!!document.querySelector('.important-button') && window.getComputedStyle(document.querySelector('.important-button')).display !== 'none'"}
|
|
582
|
+
|
|
583
|
+
// Check element exists
|
|
584
|
+
{action: "eval", payload: "!!document.querySelector('.important-button')"}
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
---
|
|
588
|
+
|
|
589
|
+
## Tips and Best Practices
|
|
590
|
+
|
|
591
|
+
### Always Wait Before Interaction
|
|
592
|
+
|
|
593
|
+
Don't interact with elements immediately after navigation:
|
|
594
|
+
|
|
595
|
+
```
|
|
596
|
+
// BAD - might fail if page slow to load
|
|
597
|
+
{action: "navigate", payload: "https://example.com"}
|
|
598
|
+
{action: "click", selector: "button"} // May fail!
|
|
599
|
+
|
|
600
|
+
// GOOD - wait for element first
|
|
601
|
+
{action: "navigate", payload: "https://example.com"}
|
|
602
|
+
{action: "await_element", selector: "button"}
|
|
603
|
+
{action: "click", selector: "button"}
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
### Use Specific Selectors
|
|
607
|
+
|
|
608
|
+
Avoid generic selectors that match multiple elements:
|
|
609
|
+
|
|
610
|
+
```
|
|
611
|
+
// BAD - matches first button on page
|
|
612
|
+
{action: "click", selector: "button"}
|
|
613
|
+
|
|
614
|
+
// GOOD - specific selector
|
|
615
|
+
{action: "click", selector: "button[type=submit]"}
|
|
616
|
+
{action: "click", selector: "button.login-button"}
|
|
617
|
+
{action: "click", selector: "#submit-form"}
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
### Verify Selectors First
|
|
621
|
+
|
|
622
|
+
Check page structure before building workflow:
|
|
623
|
+
|
|
624
|
+
```
|
|
625
|
+
// Check page HTML
|
|
626
|
+
{action: "extract", payload: "html"}
|
|
627
|
+
|
|
628
|
+
// Or check specific element
|
|
629
|
+
{action: "extract", payload: "html", selector: "form"}
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
### Handle Dynamic Content
|
|
633
|
+
|
|
634
|
+
Wait for content to load before extraction:
|
|
635
|
+
|
|
636
|
+
```
|
|
637
|
+
// BAD - tries to extract before content loads
|
|
638
|
+
{action: "navigate", payload: "https://app.com"}
|
|
639
|
+
{action: "extract", payload: "text", selector: ".user-name"} // Might be empty!
|
|
640
|
+
|
|
641
|
+
// GOOD - wait for content
|
|
642
|
+
{action: "navigate", payload: "https://app.com"}
|
|
643
|
+
{action: "await_element", selector: ".user-name"}
|
|
644
|
+
{action: "extract", payload: "text", selector: ".user-name"}
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
### Use \n for Form Submission
|
|
648
|
+
|
|
649
|
+
Append newline to auto-submit forms:
|
|
650
|
+
|
|
651
|
+
```
|
|
652
|
+
// Submit search without explicit click
|
|
653
|
+
{action: "type", selector: "#search-input", payload: "my query\n"}
|
|
654
|
+
|
|
655
|
+
// Submit login form
|
|
656
|
+
{action: "type", selector: "input[name=email]", payload: "user@example.com"}
|
|
657
|
+
{action: "type", selector: "input[name=password]", payload: "password123\n"}
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
---
|
|
661
|
+
|
|
662
|
+
## Common Pitfalls
|
|
663
|
+
|
|
664
|
+
### Don't Rely on Tab Indices
|
|
665
|
+
|
|
666
|
+
Tab indices change when tabs close — use URL or title substrings for reliable switching:
|
|
667
|
+
|
|
668
|
+
```
|
|
669
|
+
// BAD - index might be stale after closing tabs
|
|
670
|
+
{action: "switch_tab", payload: 2}
|
|
671
|
+
{action: "click", selector: "button"}
|
|
672
|
+
|
|
673
|
+
// GOOD - switch by URL or title substring (stable across tab changes)
|
|
674
|
+
{action: "switch_tab", payload: "example.com"}
|
|
675
|
+
{action: "click", selector: "button"}
|
|
676
|
+
|
|
677
|
+
// Or list tabs first to confirm the index
|
|
678
|
+
{action: "list_tabs"}
|
|
679
|
+
{action: "switch_tab", payload: 2}
|
|
680
|
+
{action: "click", selector: "button"}
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
### Increase Timeout for Slow Pages
|
|
684
|
+
|
|
685
|
+
Default timeout is 5000ms, increase if needed:
|
|
686
|
+
|
|
687
|
+
```
|
|
688
|
+
// For slow-loading elements
|
|
689
|
+
{action: "await_element", selector: ".lazy-content", timeout: 30000}
|
|
690
|
+
|
|
691
|
+
// For slow AJAX requests
|
|
692
|
+
{action: "await_text", payload: "Data loaded", timeout: 15000}
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
### Extract Structured Data with JavaScript
|
|
696
|
+
|
|
697
|
+
For complex data extraction, use JavaScript evaluation:
|
|
698
|
+
|
|
699
|
+
```
|
|
700
|
+
// Instead of multiple extract calls, use one eval
|
|
701
|
+
{action: "eval", payload: `
|
|
702
|
+
{
|
|
703
|
+
title: document.querySelector('h1').textContent,
|
|
704
|
+
author: document.querySelector('.author').textContent,
|
|
705
|
+
date: document.querySelector('time').textContent,
|
|
706
|
+
links: Array.from(document.querySelectorAll('a')).map(a => a.href)
|
|
707
|
+
}
|
|
708
|
+
`}
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
---
|
|
712
|
+
|
|
713
|
+
## Reference
|
|
714
|
+
|
|
715
|
+
- [SKILL.md](SKILL.md) - Complete tool reference
|
|
716
|
+
- [COMMANDLINE-USAGE.md](COMMANDLINE-USAGE.md) - Command-line bash examples
|
|
717
|
+
- [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) - Full protocol documentation
|