While using a browser automation agent over Model Context Protocol (MCP) to prefill job applications and publishing forms, four bottlenecks showed up in the execution logs before any site rate limit did:
click, fill, <select> elements caused multi-second stalls.Submit or Publish sent half-checked data live before a human could inspect the page.
I built turbo-browser-mcp (v0.4.0, MIT, playwright-core with no other runtime dependencies) to group those patterns into 10 consolidated MCP tools:
https://github.com/Tthanh2k6/turbo-browser-mcp Here are the four design choices in the codebase, along with where the approach still has limits.
data-tbm-ref snapshots and onlyChanged deltas
inspect_compact walks the main document, nested accessible frames, and open Shadow DOM roots (el.shadowRoot), stamps deterministic f<frameIdx>:e<N> IDs onto data-tbm-ref, and caps output at maxChars (4000 by default).
Instead of dumping a full accessibility tree after every action, the server hashes each line's signature per tab (frameIdx, tag, label, value/checked state) and supports onlyChanged: true to return only added or modified controls. Calling act_fast, batch_actions, or smart_fill_form with returnSnapshot: true folds the updated view into the same tool response so the agent does not spend a separate read turn.
smart_fill_form for React inputs, rich text, and disk uploads
Standard .value = "..." writes often vanish on React forms on blur or submit because React tracks internal input state via _valueTracker. In src/dom-engine.mjs, smart_fill_form updates inputs through the native HTMLInputElement.prototype or HTMLTextAreaElement.prototype value setter after calling _valueTracker.setValue(''), then dispatches input, change, and blur.
In the same single MCP call, fields can target:
<input> and <textarea> elements, single/multi values), <input type="range"> sliders, radios, checkboxes, and ARIA [role="combobox"] widgetscontenteditable editors (ProseMirror, Quill, compose boxes) via execCommand('insertText') with InputEvent fallbackframeIdx) and open Shadow DOM fields file or files, which Playwright attaches straight from disk through setInputFiles or filechooser (never through inline Base64 strings)allowSubmit=false guard that checks coordinates too
By default, resolveAllowSubmit evaluates to false unless allowSubmit: true is passed per call or TURBO_BROWSER_ALLOW_SUBMIT=true / --allow-submit is enabled on launch.
In src/dom-engine.mjs, SUBMIT_LABEL_RE blocks clicks, Enter key presses, and type submit: true actions matching submit, publish, post, send, apply, confirm and pay, pay, checkout, delete, and Vietnamese equivalents ( gửi, đăng bài, nộp đơn, thanh toán), plus <input type="submit"> elements outside an explicit safe list (next, continue, search, filter, save draft). Raw viewport { x, y } mouse clicks resolve the element at that screen coordinate first so a coordinate click cannot bypass the label guard.
npm run benchmark)
Measured locally on Windows (Node v24.15.0, Chromium headless) against the self-contained HTTP fixture in benchmarks/benchmark.mjs:
| Scenario | Baseline | turbo-browser-mcp |
Measured change ( o200k_base ) |
|---|---|---|---|
| Dense 140-button SaaS DOM snapshot | 18,654 tok raw HTML / 3,568 tok full ARIA | 1,135 tok compact (28 ms) / 56 tok delta (18 ms) | 93.9% vs HTML, 68.2% vs ARIA, 98.4% on delta |
| 10-field form fill + snapshot | 11 MCP turns (574 ms, 12,197 dir / 72,614 cum tok) | 1 MCP turn (82 ms, 811 tok, filled=10 ) |
11 -> 1 turns, 7x faster, 93.4% dir / 98.9% cum saved |
48 KiB PDF file upload ( 49,152 bytes) |
44,728 tok inline Base64 tool arg | 85 tok disk path (68 ms) | 99.8% tokens saved, no Base64 encode |
Open 6 tabs ( 60 ms simulated TTFB) |
6 turns (998 ms sequential) | 1 turn (420 ms parallel open_many ) |
6 -> 1 turns, 2.38x faster |
Heavy media load ( turbo vs clean) |
252 ms ( 304 KiB , 9 asset hits) |
165 ms ( 0 KiB , 0 asset hits) |
1.53x faster, 100% media bandwidth cut |
| Article Markdown + Diagnostics errors | 3,595 tok HTML / 3,460 tok all logs | 156 tok Markdown / 601 tok errors-only | 95.7% and 82.6% tokens saved |
The 405 tests across 85 suites (25 test files under test/*.test.mjs: core integration, DOM/form and session/stdio stress suites, multi-round adversarial simulations, and 10 independent audit suites) run via npm test and pass on GitHub Actions CI for Node 20 and Node 22.
Element.prototype.attachShadow so closed shadow roots created after page initialization stay inspectable and guarded, while closed roots that already existed on an attached CDP tab or live inside strict sandboxed frames fall back to Playwright frame handles, coordinate clicks, and keyboard input.network_session preset "turbo" trims asset overhead on heavy sites).open_many, capture_page, or If you use Playwright MCP or browser agents in daily workflows, what breaks most often in practice for you? I am especially curious about custom virtualized dropdowns, tricky iframe embed patterns, diagnostics you miss from Chrome DevTools, or places where SUBMIT_LABEL_RE would misclassify a safe navigation button.
Issues, PRs, and edge-case reports are welcome on GitHub:
https://github.com/Tthanh2k6/turbo-browser-mcp