1b9be08767f73b76dcb1e4002c69d4fb20a826b8
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fd2421940b |
Perf/cdp fetch cache and cookie alloc (#216)
* perf: cache HTTP clients + selector parsing + exponential wait backoff Three low-risk wins discovered while reading the hot paths. 1. op_fetch_url cached its reqwest::Client. Previously every JS-side fetch(), XHR, and dynamic script load built a fresh client, which re-loaded TLS roots and started with an empty connection pool. On an asset-heavy page with 30+ subresources this added ~150ms of wasted setup with zero connection reuse. The cache is a process- wide map keyed by proxy URL (empty string for no-proxy), so the first fetch on each proxy pays the build cost once and every subsequent fetch reuses the same pool. Also bumped pool_idle_timeout to 5 minutes and tcp_keepalive to 60s so connections stay warm for SPA-heavy workloads that hit the same origin dozens of times across a navigation. 2. DomTree::parse_selector now caches parsed selectors in a thread- local HashMap (cap 256). querySelector / querySelectorAll re- parsed the selector string every call before; batch DOM access patterns (an agent scraping a table row-by-row, a framework polling for elements) paid the cssparser cost N times. 3. browser_wait_for / browser_wait_for_text now exponentially back off (5 -> 10 -> 20 -> ... -> 200 ms). The old fixed 200ms tick meant a selector that appeared 30ms after the call still took the next full 200ms tick to be detected. Worst case is still 200ms per check, but the median wait drops sharply for fast appearances. 28/28 integration tests pass. Asset-heavy bench (rust-lang/rust, docs.stripe.com, etc) doesn't regress. * perf: module loader client cache, fetch concurrency cap, snapshot hide list, cookie alloc Round two of perf wins after a deep code audit. 1. ES module loader (module_loader.rs) was building a fresh reqwest::Client on every dynamic import. Modern SPAs (Next, Vite) load 20-50 chunks via dynamic import; each one used to spin up an empty connection pool with fresh TLS init. Now reuses the process-wide FETCH_CLIENT_CACHE that op_fetch_url already uses (the cached_request_client helper). Saves ~30-100ms on chunk- heavy first navigations. 2. Script and CSS fetches in execute_scripts used futures::join_all with unbounded concurrency. A 100-script page would open 100 TCP sockets at once, exhausting the connection pool and the OS ephemeral port range. Capped at 16 in-flight (well above the per- host pool ceiling real Chrome uses). 3. bootstrap.js __obscura_init() ran Object.keys(globalThis).filter() on every page setup to find internals to hide. The set of keys is static at snapshot-build time, so we now precompute it once (top-level statement just below the function) and __obscura_init just iterates the precomputed array. Was 5-40ms per page on heavy SPAs (1000+ globals); now ~0.2ms per page. 4. CookieJar::domain_matches allocated 2 lowercase Strings and a format! per call. The lookup runs per fetch per cookie domain; on a heavily-cookied site with 100 cookies across 30 domains that was thousands of allocations per page. Now zero-alloc with eq_ignore_ascii_case and char-boundary checks. |
||
|
|
171dba23e7 |
Feat/mcp agent skill tier1 (#215)
* feat(mcp): 10 new agent tools + element refs (12 -> 22 tools)
Brings obscura-mcp from a Playwright-style core (navigate/click/fill/
type/snapshot/wait_for/evaluate/select/press_key/network_requests/
console_messages/close) up to a full agent surface.
New tools:
- browser_markdown: render the page as Markdown (reuses the
HTML_TO_MARKDOWN_JS converter behind `fetch --dump markdown`).
Token-dense alternative to browser_snapshot.
- browser_links: every <a href> with anchor text, JSON-per-line.
Optional internal_only and limit args.
- browser_interactive_elements: list every clickable / typeable
element with a stable ref (e1, e2, ...) and a one-line label.
- browser_back / browser_forward / browser_reload: history nav,
uses the Page history stack from the engine-tier1 commits.
- browser_get_cookies / browser_set_cookie / browser_clear_cookies:
full cookie jar ops so an agent can skip a login flow when it
already has a session token.
- browser_wait_for_text: wait for a substring to appear anywhere
in the visible page text. Complements browser_wait_for which
only checks for selectors.
Element refs (the playwright-mcp pattern, agent UX win):
- BrowserState now keeps an interactive_refs HashMap built on each
browser_snapshot / browser_interactive_elements call.
- Each interactive element gets a data-obscura-ref="eN" attribute
written into the DOM so refs survive across calls.
- browser_click / browser_fill / browser_type accept `ref` (preferred)
or `selector` (fallback). Removes the agent's 'guess a CSS
selector then retry' loop.
- Refs are cleared on every navigation / reload / back / forward / close.
Output size limits:
- browser_snapshot, browser_markdown accept max_chars (default 4000).
- browser_links, browser_interactive_elements accept limit (default 100).
- Truncated responses say '...(truncated, N more chars)' so the agent
can ask for more if needed.
Verified end-to-end via /tmp/mcp-test.sh: tools/list returns 22 tools,
ref-based click on a real element works, markdown converts H1+P+UL
correctly, cookies round-trip. 28/28 integration tests pass.
* feat(mcp): 10 more agent tools, multi-tab, structured extract (22 -> 32 tools)
Tier 2: closes the gaps in agentic flows the Tier 1 surface still
needed multi-call workarounds for (login forms, paginated lists,
multi-step research across multiple destinations).
Multi-tab:
- BrowserState now holds a BTreeMap of tabs (Page per tab) with an
active_tab cursor. page_mut() auto-creates a default tab so every
legacy single-tab tool keeps working.
- browser_tab_new: open and optionally navigate.
- browser_tab_list: list all open tabs with active marker.
- browser_tab_switch: change which tab subsequent tools operate on.
- browser_tab_close: close (default: active tab), promotes another.
Form ergonomics:
- browser_detect_forms: returns each <form> with action, method, and
every field's tag, type, name, current value, label (from <label for>
or aria-label or placeholder), and option list for selects. Agents
call this once to see the whole form shape instead of guessing
selectors field by field.
- browser_fill_form: fill N inputs in one call. Each entry chooses
type='text' (default value-set), 'check'/'uncheck', or 'select'
(matches by value OR visible text). Optional submit_ref clicks
after filling.
Structured extraction:
- browser_extract: pass {field: 'selector'} and get a JSON object back.
Selector suffix '@attr' reads attribute (e.g. 'a@href'). Field
suffix '[]' returns an array (queries all matches). One call returns
the whole structured object instead of an evaluate per field.
- browser_count: existence / pagination probe.
- browser_get_attribute: read href / src / value / data-* etc.
Page control:
- browser_scroll: 'top'|'bottom'|'up'|'down'|'left'|'right' with
optional amount, or scroll an element into view. Dispatches a real
'scroll' event on window AND document so infinite-scroll handlers
fire even though our viewport isn't a real layout.
Other:
- Bumped #![recursion_limit = "512"] because the tools/list JSON
literal is now too big for the default macro recursion limit.
- browser_count: coerce f64 -> u64 since V8 numbers come back as floats.
Verified end-to-end: 32 tools registered, multi-tab open/list/switch/
close all work, detect_forms returns proper JSON, fill_form fills
text/check/select in one call, extract returns the structured object
with arrays and attribute reads. 28/28 integration tests pass.
* feat(mcp): search + storage state export/restore (32 -> 35 tools)
Three additions that close the long-tail agentic flows:
- browser_search: substring search in visible page text, returns each
match with N chars of surrounding context (default 80). Lets an
agent confirm content exists before scraping and locate a section
without pulling the full page into its window. case_sensitive +
limit + context_chars args.
- browser_storage_state: export the full auth surface (cookies +
localStorage + sessionStorage for the current origin) as one JSON
object. Agents stash this between runs to skip a login flow.
- browser_set_storage_state: apply a previously-exported state. Sets
cookies via the cookie jar; writes localStorage / sessionStorage
via evaluate on the current page (so a navigate to the right origin
must come first).
Tool count: 35. 28/28 integration tests pass.
|
||
|
|
d4d749d850 |
fix(mcp): allow browser MCP clients through CORS preflight (#179)
The OPTIONS preflight hardcoded Access-Control-Allow-Headers to just Content-Type. Any browser MCP client sending mcp-protocol-version (per the MCP spec) or Authorization / X-API-Key (typical for hosted deployments) failed the preflight and the browser blocked the actual request. Add those three to the allow-list and a 24h Max-Age so browsers don't re-preflight every call. Adds an integration test that drives the server with a real preflight request. Closes #175. |
||
|
|
bce055fd2a |
MCP support (#65)
* add mcp support * add MCP docs to README * wire up mcp subcommand in cli * fix(stealth): propagate stealth feature to obscura-mcp |