Stealth web browser for agents: search, fetch, click, download and type in persistent MCP sessions.
net.aginx/aginxbrowser — Model Context Protocol (MCP) Server
This MCP-compatible server provides a stealth web browser for agents. It supports agent-driven web interactions including search, fetch, click, download, and typing within persistent MCP sessions. The repository describes “AginxBrowser,” including an MCP-compatible endpoint and a hosted browser at browser.aginx.net.
🛠️ Key Features
Stealth web browser for agents
Search, fetch, click, download, and type actions
Persistent MCP sessions
MCP-compatible integration (as stated in the README excerpt)
Topics include anti-bot/anti-detect and browser-fingerprinting measures
🚀 Use Cases
Agent-based web browsing and automation
Web scraping workflows requiring navigation and interaction
Agent tasks that need persistent session state
⚡ Developer Benefits
MCP-compatible tool integration for model-driven browser actions
Hosted access referenced via browser.aginx.net
Implementation oriented around agent browsing operations
⚠️ Limitations
The provided excerpt does not document specific technical constraints, authentication details, or supported targets beyond general agent browsing actions
Delete a named login identity: stored record AND live jar. Cookie values are credentials — delete means gone. Sessions currently running as the account keep their in-process jar handle, but nothing writes back. Returns {deleted: name}, or an error naming the account if it does not exist.
Parameters1
name
string
required
The account to delete: stored record AND live jar.
Raw schema
{
"type": "object",
"properties": {
"name": {
"description": "The account to delete: stored record AND live jar.",
"type": "string"
}
},
"required": [
"name"
],
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
account_list
List named login identities (the multi-account layer) with metadata only: name, cookie domains, cookie count, updated_at, the last account_verify verdict, and the identity's persona User-Agent (each account is one stable device: its own UA and hardware fingerprint, drawn once and reused). Cookie values are credentials and never leave the server. Use to see which identities exist before session_create {account} picks one.
Parameters
No parameters.
Raw schema
{
"type": "object",
"properties": {}
}
account_verify
Check whether a named account is still logged in. Teach-once: the first call passes url + predicate (a JS expression truthy on a logged-in page, e.g. !!document.querySelector('.user-nick')); the spec is remembered and later calls can be bare. Runs in a scratch session AS the account (private jar), so the probe doubles as a cookie refresh. Returns {name, logged_in, url, checked_at}.
Parameters3
name
string
required
The account to check.
url
string | null
optional
Teach-once: the page that shows login state (its login wall if the
account is logged out). Remembered after the first call.
predicate
string | null
optional
Teach-once: a JS expression that is truthy when logged in, e.g.
`!!document.querySelector('.user-nick')`. Remembered after the first
call — later calls can pass neither and rerun the spec.
Raw schema
{
"type": "object",
"properties": {
"name": {
"description": "The account to check.",
"type": "string"
},
"url": {
"description": "Teach-once: the page that shows login state (its login wall if the\naccount is logged out). Remembered after the first call.",
"type": [
"string",
"null"
],
"default": null
},
"predicate": {
"description": "Teach-once: a JS expression that is truthy when logged in, e.g.\n`!!document.querySelector('.user-nick')`. Remembered after the first\ncall — later calls can pass neither and rerun the spec.",
"type": [
"string",
"null"
],
"default": null
}
},
"required": [
"name"
],
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
cache
Query the LOCAL CACHE of every page this server has fetched and every search it has run. Check here BEFORE re-fetching or re-searching — a hit is instant and free while a fresh fetch costs 5-60s. Use query for full-text search (works for Chinese substrings and English words), get to pull a page's full cached content, stats for counts, clear to delete rows.
Parameters9
query
string | null
optional
Full-text search over cached page contents, titles, URLs and past search queries. Omit to list the latest rows.
url
string | null
optional
Only rows whose URL contains this substring
get
string | null
optional
Return the FULL cached content of this exact URL instead of listing hits
kind
string | null
optional
Which rows to search: "auto" (default, pages + searches), "pages", or "searches"
since_hours
integer | null
optional
Only rows stored within the last N hours
limit
integer
optional
Maximum rows returned (default: 10, max 100)
stats
boolean
optional
Return row counts and database size instead of rows
clear
boolean
optional
Delete matching rows instead of returning them (requires url, since_hours, or all)
all
boolean
optional
With clear: delete everything cached for this caller
Raw schema
{
"type": "object",
"properties": {
"query": {
"description": "Full-text search over cached page contents, titles, URLs and past search queries. Omit to list the latest rows.",
"type": [
"string",
"null"
],
"default": null
},
"url": {
"description": "Only rows whose URL contains this substring",
"type": [
"string",
"null"
],
"default": null
},
"get": {
"description": "Return the FULL cached content of this exact URL instead of listing hits",
"type": [
"string",
"null"
],
"default": null
},
"kind": {
"description": "Which rows to search: \"auto\" (default, pages + searches), \"pages\", or \"searches\"",
"type": [
"string",
"null"
],
"default": null
},
"since_hours": {
"description": "Only rows stored within the last N hours",
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 0,
"default": null
},
"limit": {
"description": "Maximum rows returned (default: 10, max 100)",
"type": "integer",
"format": "uint",
"minimum": 0,
"default": 10
},
"stats": {
"description": "Return row counts and database size instead of rows",
"type": "boolean",
"default": false
},
"clear": {
"description": "Delete matching rows instead of returning them (requires url, since_hours, or all)",
"type": "boolean",
"default": false
},
"all": {
"description": "With clear: delete everything cached for this caller",
"type": "boolean",
"default": false
}
},
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
click
Click an element on a one-off page: loads `url` in a fresh browser context (stateless — no cookies unless passed, no shared state with other calls), waits `wait_secs` after load before clicking, then fires a DOM click on the first CSS-selector match. The click may trigger navigation (link, form submit) — the response `url` and `text_after` are read after that navigation lands. Returns `clicked:false` when the selector matches nothing. For multi-step interaction on a shared page use session_click instead.
Parameters3
url
string
required
The URL to load
selector
string
required
CSS selector of element to click
wait_secs
integer | null
optional
Seconds to wait for the page to settle after load, before clicking
Raw schema
{
"type": "object",
"properties": {
"url": {
"description": "The URL to load",
"type": "string"
},
"selector": {
"description": "CSS selector of element to click",
"type": "string"
},
"wait_secs": {
"description": "Seconds to wait for the page to settle after load, before clicking",
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 0,
"default": null
}
},
"required": [
"url",
"selector"
],
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
download
Download a file over HTTP(S) with streaming to disk (no memory buffering), SHA-256 integrity hash, and optional resume of interrupted transfers. Filename resolution: explicit param → Content-Disposition → URL tail. Use for binaries, archives, datasets, documents - anything where the agent wants the FILE saved, not its text content read.
Resume an interrupted download when a local partial file exists
use_proxy
boolean
optional
Route through proxy (default: false; auto-enabled for known blocked domains)
cookies
array
optional
Cookies to send with the request: `"name=value"` strings or
CDP-style objects `{"name","value","domain",...}` for gated downloads
Raw schema
{
"type": "object",
"properties": {
"url": {
"description": "URL of the file to download (http/https)",
"type": "string"
},
"filename": {
"description": "Explicit output filename. When omitted: Content-Disposition → URL tail → \"download\"",
"type": [
"string",
"null"
],
"default": null
},
"resume": {
"description": "Resume an interrupted download when a local partial file exists",
"type": "boolean",
"default": false
},
"use_proxy": {
"description": "Route through proxy (default: false; auto-enabled for known blocked domains)",
"type": "boolean",
"default": false
},
"cookies": {
"description": "Cookies to send with the request: `\"name=value\"` strings or\nCDP-style objects `{\"name\",\"value\",\"domain\",...}` for gated downloads",
"type": "array",
"items": {
"type": "string"
},
"default": []
}
},
"required": [
"url"
],
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
eval
Execute JavaScript on a one-off page: loads `url` in a fresh browser context, optionally waits `wait_secs` for the page to settle, evaluates `script` (async/Promise supported) and returns `{url, result}`. Script-driven navigation (location.href, form submit) is drained and reflected in the returned `url`. Stateless — no cookies or page state shared with other calls; when the script needs prior page state or a login, use session_eval.
Parameters3
url
string
required
The URL to load
script
string
required
JavaScript code to execute (supports async/Promise)
Fetch a webpage and return clean markdown/html/text. Use whenever the agent needs to READ any web page - blogs, docs, articles, JS-rendered SPAs, Cloudflare-protected sites. Static pages are served over plain HTTP (~100ms tier:"http"); pages that need JS get the full browser (tier:"browser"). render_tier selects auto (default) / http (pure HTTP, refuses the upgrade) / obscura (always browser).
Parameters12
url
string
required
The URL to fetch
format
string
optional
Output format: "markdown", "html", or "text" (default: markdown)
selector
string | null
optional
CSS selector to extract specific content
wait_secs
integer | null
optional
Seconds to wait for JS rendering
use_proxy
boolean
optional
Route through proxy (for blocked foreign sites)
max_chars
integer
optional
Maximum characters to return (default: 50000)
auto_bypass_challenge
boolean
optional
Auto-detect and bypass Cloudflare Turnstile challenges (default: true)
render_tier
any
optional
Rendering strategy: "auto" (default), "http", or "obscura"
tls_fingerprint
string | null
optional
TLS fingerprint override (stealth mode only): "chrome145", "firefox133", etc.
js_extract
any
optional
JS expression to extract from the page after rendering
sanitize
boolean
optional
Strip prompt-injection payloads from the text output (default true):
zero-width/steganographic characters, instruction-shaped lines
("ignore previous instructions", chat markup tokens, CJK variants),
and text hidden via opacity:0 / tiny fonts. A `sanitize_report`
field counts what was removed — stripping is observable, never
silent. Set false for raw output.
capture_xhr
array | null
optional
Capture script-initiated API responses: a list of URL substrings
(e.g. ["/api/"]) whose matching fetch/XHR bodies come back in an
`xhr` array; an empty list captures every XHR/Fetch. Forces browser
rendering (script-initiated requests only exist after JS runs).
Raw schema
{
"type": "object",
"properties": {
"url": {
"description": "The URL to fetch",
"type": "string"
},
"format": {
"description": "Output format: \"markdown\", \"html\", or \"text\" (default: markdown)",
"type": "string",
"default": "markdown"
},
"selector": {
"description": "CSS selector to extract specific content",
"type": [
"string",
"null"
],
"default": null
},
"wait_secs": {
"description": "Seconds to wait for JS rendering",
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 0,
"default": null
},
"use_proxy": {
"description": "Route through proxy (for blocked foreign sites)",
"type": "boolean",
"default": false
},
"max_chars": {
"description": "Maximum characters to return (default: 50000)",
"type": "integer",
"format": "uint",
"minimum": 0,
"default": 50000
},
"auto_bypass_challenge": {
"description": "Auto-detect and bypass Cloudflare Turnstile challenges (default: true)",
"type": "boolean",
"default": true
},
"render_tier": {
"description": "Rendering strategy: \"auto\" (default), \"http\", or \"obscura\"",
"$ref": "#/$defs/RenderTier",
"default": "auto"
},
"tls_fingerprint": {
"description": "TLS fingerprint override (stealth mode only): \"chrome145\", \"firefox133\", etc.",
"type": [
"string",
"null"
],
"default": null
},
"js_extract": {
"description": "JS expression to extract from the page after rendering",
"anyOf": [
{
"$ref": "#/$defs/JsExtractParams"
},
{
"type": "null"
}
],
"default": null
},
"sanitize": {
"description": "Strip prompt-injection payloads from the text output (default true):\nzero-width/steganographic characters, instruction-shaped lines\n(\"ignore previous instructions\", chat markup tokens, CJK variants),\nand text hidden via opacity:0 / tiny fonts. A `sanitize_report`\nfield counts what was removed — stripping is observable, never\nsilent. Set false for raw output.",
"type": "boolean",
"default": true
},
"capture_xhr": {
"description": "Capture script-initiated API responses: a list of URL substrings\n(e.g. [\"/api/\"]) whose matching fetch/XHR bodies come back in an\n`xhr` array; an empty list captures every XHR/Fetch. Forces browser\nrendering (script-initiated requests only exist after JS runs).",
"type": [
"array",
"null"
],
"items": {
"type": "string"
},
"default": null
}
},
"required": [
"url"
],
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"RenderTier": {
"description": "Tiered rendering strategy selector.",
"oneOf": [
{
"description": "HTTP-direct first, fall back to diting browser. (default)",
"type": "string",
"const": "auto"
},
{
"description": "Pure HTTP, no V8/JS. Fastest; misses JS-rendered content.",
"type": "string",
"const": "http"
},
{
"description": "Always use the diting browser (current behaviour pre-tiering).\n\"browser\" is accepted as an alias — agents guess it before \"obscura\".",
"type": "string",
"const": "obscura"
}
]
},
"JsExtractParams": {
"type": "object",
"properties": {
"expression": {
"description": "JavaScript expression to evaluate (e.g. \"window.__INITIAL_STATE__\")",
"type": "string"
},
"timeout_ms": {
"description": "Timeout in milliseconds (default: 5000)",
"type": "integer",
"format": "uint64",
"minimum": 0,
"default": 5000
}
},
"required": [
"expression"
]
}
}
}
flow_run
Run a flow — a recorded, editable JSON browser-session script — deterministically, with zero model tokens. Steps are {op, args, expect?, save?}: ops cover navigate/click/click_xy/input/scroll/eval/wait/screenshot/state/cookies; {{var}} placeholders in args are filled from vars; expect asserts (url_contains | selector | text_contains | eval_truthy) abort with evidence on failure; save collects a step's output into the receipt. Source the flow inline via "flow", or by "name" from the server's workflow/<name>/flow.json (unknown name → error lists installed workflows). Pass session_id to reuse a live session (e.g. from import_curl) so login state and flows compose. The receipt carries status ok/failed, saved outputs, the session_id (kept alive), and on failure the failing step, reason and a diagnostic screenshot — fix the flow or take the session over from there.
Or run a server-side workflow/<name>/flow.json asset. An unknown name
errors back with the list of installed workflows — that error is the
discovery call.
vars
any
optional
Values for {{placeholders}} in step args; wins over the flow's own
vars defaults.
session_id
string | null
optional
Reuse a live session (e.g. from import_curl) instead of creating a
fresh one — that's how login state and flows compose.
Raw schema
{
"type": "object",
"properties": {
"flow": {
"description": "Inline flow document: {create?, vars?, steps:[{op, args, expect?, save?}]}",
"default": null
},
"name": {
"description": "Or run a server-side workflow/<name>/flow.json asset. An unknown name\nerrors back with the list of installed workflows — that error is the\ndiscovery call.",
"type": [
"string",
"null"
],
"default": null
},
"vars": {
"description": "Values for {{placeholders}} in step args; wins over the flow's own\nvars defaults.",
"default": null
},
"session_id": {
"description": "Reuse a live session (e.g. from import_curl) instead of creating a\nfresh one — that's how login state and flows compose.",
"type": [
"string",
"null"
],
"default": null
}
},
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
import_curl
Import login state from a real browser in one paste. The human logs into a site in their own Chrome (solving the CAPTCHA/SMS once), opens DevTools → Network, right-clicks any authenticated request → "Copy as cURL", and passes the command here. Returns a live session_id already carrying that site's cookies and sitting on the copied request's URL — the agent continues from where the human left off, no password or second login needed. Works with bash, PowerShell and cmd copy flavors.
Parameters3
curl
string
required
A "Copy as cURL" command pasted from Chrome DevTools (Network panel →
right-click any authenticated request). bash, PowerShell and cmd
flavors all parse; the cookie set is injected and the session
navigates to the copied request's URL.
use_proxy
boolean
optional
Route the session's traffic through the engine proxy.
account
string | null
optional
Attach the session to a named account: the imported login lands in
the account's private jar and is written back under its name after
every action — one import per identity, no clobbering.
Raw schema
{
"type": "object",
"properties": {
"curl": {
"description": "A \"Copy as cURL\" command pasted from Chrome DevTools (Network panel →\nright-click any authenticated request). bash, PowerShell and cmd\nflavors all parse; the cookie set is injected and the session\nnavigates to the copied request's URL.",
"type": "string"
},
"use_proxy": {
"description": "Route the session's traffic through the engine proxy.",
"type": "boolean",
"default": false
},
"account": {
"description": "Attach the session to a named account: the imported login lands in\nthe account's private jar and is written back under its name after\nevery action — one import per identity, no clobbering.",
"type": [
"string",
"null"
],
"default": null
}
},
"required": [
"curl"
],
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
render_markdown
Render a markdown document into a deterministic, self-contained HTML artifact - the document layer, so the agent never writes HTML by hand. Prose rides a plain offline shell (no fonts, no scripts); archify fenced code blocks carry typed zero-coordinate diagram JSON (sequence, workflow, architecture, dataflow, lifecycle families) and render to inline SVG via the layout engine. Same input, same bytes: the receipt carries the sha256 so determinism is verifiable. theme picks light (default) or dark; preset picks the palette family — classic (default), signal-flow, blueprint, editorial — orthogonal to theme; colors bake at generation time (presentation attributes, not CSS variables), and the receipt records both preset and theme. quality picks the composition audit profile — standard (default) or showcase, the delivery gate: the receipt's diagrams[].composition grades route crossings, ambiguous corridors, label clearance (2px standard / 4px showcase), route rhythm, and node text projected to the 930px reader width; the audit never changes the artifact bytes. Mermaid sources are the agent's job to translate, not the engine's: flowchart/graph → workflow (lanes + columns), sequenceDiagram → sequence, stateDiagram-v2 → lifecycle (bands), erDiagram/class → architecture (grid + boundaries) — read the topology and emit the matching zero-coordinate archify JSON; the engine accepts only archify JSON. A broken diagram degrades to a visible code block and lands in receipt.diagnostics; an authored route preset that cannot be honored is self-repaired to a verified semantic substitute and disclosed in receipt diagrams[].repairs - the document still renders. A fence may also carry views: [{id,label,nodes,note?}] (node ids of the active family), emitted as guided-view tabs above the diagram plus an inlined viewer script - clicking a tab lights the member nodes and the routes between them (subgraph), clicking a node lights it with its direct neighbors (ego graph), everything else dims; a view's optional note shows as a caption while it is active (the story layer). window.agxViewer in a session drives and reads the same state programmatically: {focus,view,state} as before, plus route(i,from,to) which returns and lights the shortest authored directed path between two nodes (null when unreachable, state untouched), and reach(i,id,down|up) which returns and lights the authored downstream/upstream closure ({nodes,links}); both dim the rest of the diagram. diagrams[].views in the receipt lists the tabs. motion: true bakes an entrance choreography into the artifact: pure-declarative CSS animation with zero scripts - headings split into per-glyph (CJK) / per-word (latin) spans that rise in with expo easing, prose blocks stagger up an nth-child delay ladder, diagram figures grow in with a back ease (GSAP's easing math as public cubic-bezier equivalents, nothing embedded); the diagrams themselves play a flow story on the same clock - nodes land beat by beat, solid edges draw in (dash-offset), dashed returns fade, sequence messages arrive as sent - with a timed caption strip under each figure as the subtitles, which becomes a static transcript under prefers-reduced-motion; the file itself animates in any browser and the receipt records motion plus diagrams[].story (beat times and captions - the hook for muxing voice later). With session_id the artifact is also loaded into that session (local, free) and the reply carries viewport acceptance: scroll extents measured in the live session and graded fits/tall/wide/oversized, telling the agent how to read the page back. Diagram vocabulary adapted from archify (MIT).
Parameters6
markdown
string
required
Full markdown document. Prose rides a plain offline shell; archify
fenced code blocks carry typed zero-coordinate diagram JSON and
render to inline SVG.
theme
string | null
optional
Color theme: "light" (default) or "dark" — the shell background/
foreground and every SVG palette slot swap together; the receipt
records which theme produced the bytes
preset
string | null
optional
Visual preset: "classic" (default), "signal-flow", "blueprint", or
"editorial" — a palette family orthogonal to theme (each preset
exists in both light and dark). The receipt records preset and
theme separately
session_id
string | null
optional
Optional session ID: also load the rendered HTML into that live
session (local and free) so session_screenshot / session_state can
verify the artifact
quality
string | null
optional
Quality profile for the composition audit: "standard" (default) or
"showcase" — the delivery gate. The audit grades route crossings,
corridors, label clearance, rhythm, and projected text size in the
receipt (diagrams[].composition); it never changes the artifact
bytes, only how findings are severity-rated
motion
boolean | null
optional
Bake the entrance choreography into the artifact (default false):
pure-declarative CSS animation — headings split into per-glyph/per-
word spans that rise in with expo easing, prose blocks stagger up a
nth-child delay ladder, and diagram figures grow in with a back
ease (GSAP's easing math as public cubic-bezier equivalents). The
diagrams animate too, on one story clock: nodes pop in one beat at
a time, solid edges draw themselves (dash-offset drain), dashed
returns fade, sequence messages land as they are "sent", and a
timed caption strip under each figure subtitles the beats — under
prefers-reduced-motion the strip becomes a static transcript.
Zero scripts: the file itself animates in any browser, subtitles
and all; the receipt records motion (plus diagrams[].story with
the beat times, the hook for muxing voice later) so a cached
artifact is never mistaken for the static one
Raw schema
{
"type": "object",
"properties": {
"markdown": {
"description": "Full markdown document. Prose rides a plain offline shell; archify\nfenced code blocks carry typed zero-coordinate diagram JSON and\nrender to inline SVG.",
"type": "string"
},
"theme": {
"description": "Color theme: \"light\" (default) or \"dark\" — the shell background/\nforeground and every SVG palette slot swap together; the receipt\nrecords which theme produced the bytes",
"type": [
"string",
"null"
]
},
"preset": {
"description": "Visual preset: \"classic\" (default), \"signal-flow\", \"blueprint\", or\n\"editorial\" — a palette family orthogonal to theme (each preset\nexists in both light and dark). The receipt records preset and\ntheme separately",
"type": [
"string",
"null"
]
},
"session_id": {
"description": "Optional session ID: also load the rendered HTML into that live\nsession (local and free) so session_screenshot / session_state can\nverify the artifact",
"type": [
"string",
"null"
]
},
"quality": {
"description": "Quality profile for the composition audit: \"standard\" (default) or\n\"showcase\" — the delivery gate. The audit grades route crossings,\ncorridors, label clearance, rhythm, and projected text size in the\nreceipt (diagrams[].composition); it never changes the artifact\nbytes, only how findings are severity-rated",
"type": [
"string",
"null"
]
},
"motion": {
"description": "Bake the entrance choreography into the artifact (default false):\npure-declarative CSS animation — headings split into per-glyph/per-\nword spans that rise in with expo easing, prose blocks stagger up a\nnth-child delay ladder, and diagram figures grow in with a back\nease (GSAP's easing math as public cubic-bezier equivalents). The\ndiagrams animate too, on one story clock: nodes pop in one beat at\na time, solid edges draw themselves (dash-offset drain), dashed\nreturns fade, sequence messages land as they are \"sent\", and a\ntimed caption strip under each figure subtitles the beats — under\nprefers-reduced-motion the strip becomes a static transcript.\nZero scripts: the file itself animates in any browser, subtitles\nand all; the receipt records motion (plus diagrams[].story with\nthe beat times, the hook for muxing voice later) so a cached\nartifact is never mistaken for the static one",
"type": [
"boolean",
"null"
]
}
},
"required": [
"markdown"
],
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
render_pdf
Cut a rendered page into pages and package as PDF, PNGs, PPTX or DOCX. Print mode (no selector) paginates the document into fixed-height pages (default 794x1123, A4 @96dpi), breaking at top-level block boundaries — no half-cut text where a break can land on a block edge. Slides mode (selector set) makes one page per match, sized to that element — generate an HTML deck with one .slide per page and each becomes a deck page. format "pdf" (default) returns base64 image-based PDF; "png" returns one base64 PNG per page in pages_base64; "pptx" returns a base64 PPTX (one slide per page, deck-sized to the largest page); "docx" returns a base64 DOCX (one page-sized section per page, each section keeps its own height). Returns page count and packaging.
Parameters9
url
string
required
Page URL to cut into pages.
format
string
optional
Output format: "pdf" (default), "png" (one base64 PNG per page),
"pptx" (one slide per page, image-based), "pptx-native" (editable:
element-level DrawingML — real text runs, gradient shapes, image
parts; requires `selector`), or "docx" (one page-sized section per
page).
width
integer
optional
Page width in CSS pixels. Default 794 (A4 @96dpi).
Render a page's animation timelines to an MP4 video. The page's scripts must expose `window.__timelines` — objects with `duration()` and `pause(t)` (a paused gsap.timeline registered there works as-is). Each frame seeks every timeline to t=i/fps and paints the viewport, so the output is deterministic — no wall clock in the pixel values. Audio: `narration[]` places TTS/voice clips at start times (mixed into one AAC track), `audio` adds looped background music, and `subtitles_srt` muxes an SRT as a soft mov_text track and (by default, `burn_subtitles: false` to opt out) burns the same cues into the frame pixels — QuickTime, WeChat and most social embeds ignore the soft track. Requires ffmpeg on the server. Returns base64 MP4 (H.264, yuv420p) plus frame count and durations.
Parameters14
url
string
required
Page URL whose scripts register timelines in `window.__timelines`
(GSAP-style objects with `duration()` + `pause(t)`).
fps
number
optional
Frames per second. Default 24.
width
integer
optional
Viewport width in CSS pixels (floored to even — yuv420p). Default 1280.
height
integer
optional
Viewport height in CSS pixels. Default 720.
hold_tail_secs
number
optional
Freeze the final timeline state for this many extra seconds. Default 0.5.
max_duration_secs
number
optional
Safety cap on timeline + hold tail, seconds. Default 120.
wait_timelines_ms
integer
optional
How long to wait for `window.__timelines` to appear, ms. Default 10000.
use_proxy
boolean
optional
Route through proxy (for blocked foreign sites)
tls_fingerprint
string | null
optional
TLS fingerprint override (stealth mode only)
audio
any
optional
Background music: looped to cover the video, volume-scaled, faded
out at the tail.
narration
array
optional
Voiceover clips, each starting at its own time (any TTS output;
mixed into one AAC track).
subtitles_srt
string | null
optional
Inline SRT subtitles muxed as a soft (toggleable) mov_text track.
subtitles_language
string | null
optional
ISO language tag for the subtitle track, e.g. "eng" / "zh".
burn_subtitles
boolean | null
optional
Burn the cues into the frame pixels too (hardsub) — on by default
when `subtitles_srt` is present; QuickTime, WeChat and most social
embeds ignore the soft mov_text track. `false` keeps the soft track
only.
Raw schema
{
"type": "object",
"properties": {
"url": {
"description": "Page URL whose scripts register timelines in `window.__timelines`\n(GSAP-style objects with `duration()` + `pause(t)`).",
"type": "string"
},
"fps": {
"description": "Frames per second. Default 24.",
"type": "number",
"format": "double",
"default": 24
},
"width": {
"description": "Viewport width in CSS pixels (floored to even — yuv420p). Default 1280.",
"type": "integer",
"format": "uint32",
"minimum": 0,
"default": 1280
},
"height": {
"description": "Viewport height in CSS pixels. Default 720.",
"type": "integer",
"format": "uint32",
"minimum": 0,
"default": 720
},
"hold_tail_secs": {
"description": "Freeze the final timeline state for this many extra seconds. Default 0.5.",
"type": "number",
"format": "double",
"default": 0.5
},
"max_duration_secs": {
"description": "Safety cap on timeline + hold tail, seconds. Default 120.",
"type": "number",
"format": "double",
"default": 120
},
"wait_timelines_ms": {
"description": "How long to wait for `window.__timelines` to appear, ms. Default 10000.",
"type": "integer",
"format": "uint64",
"minimum": 0,
"default": 10000
},
"use_proxy": {
"description": "Route through proxy (for blocked foreign sites)",
"type": "boolean",
"default": false
},
"tls_fingerprint": {
"description": "TLS fingerprint override (stealth mode only)",
"type": [
"string",
"null"
],
"default": null
},
"audio": {
"description": "Background music: looped to cover the video, volume-scaled, faded\nout at the tail.",
"anyOf": [
{
"$ref": "#/$defs/RenderVideoAudio"
},
{
"type": "null"
}
],
"default": null
},
"narration": {
"description": "Voiceover clips, each starting at its own time (any TTS output;\nmixed into one AAC track).",
"type": "array",
"items": {
"$ref": "#/$defs/RenderNarrationClip"
},
"default": []
},
"subtitles_srt": {
"description": "Inline SRT subtitles muxed as a soft (toggleable) mov_text track.",
"type": [
"string",
"null"
],
"default": null
},
"subtitles_language": {
"description": "ISO language tag for the subtitle track, e.g. \"eng\" / \"zh\".",
"type": [
"string",
"null"
],
"default": null
},
"burn_subtitles": {
"description": "Burn the cues into the frame pixels too (hardsub) — on by default\nwhen `subtitles_srt` is present; QuickTime, WeChat and most social\nembeds ignore the soft mov_text track. `false` keeps the soft track\nonly.",
"type": [
"boolean",
"null"
],
"default": null
}
},
"required": [
"url"
],
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"RenderVideoAudio": {
"description": "render_video `audio`: background music track — fetched, looped to cover\nthe video, volume-scaled, optionally faded out at the end.",
"type": "object",
"properties": {
"url": {
"description": "URL of the music file. mp3/wav/ogg/m4a — probed by content.",
"type": "string"
},
"volume": {
"description": "Linear multiplier 0..=2; 1 = as authored. Default 1.",
"type": "number",
"format": "float",
"default": 1
},
"fade_out_secs": {
"description": "Fade out over the final N seconds. Default 0 (none).",
"type": "number",
"format": "float",
"default": 0
},
"loop_audio": {
"description": "Loop to cover the whole video. Default true.",
"type": "boolean",
"default": true
}
},
"required": [
"url"
]
},
"RenderNarrationClip": {
"description": "render_video `narration[]`: one voiceover clip placed at a start time —\ngenerate with any TTS, hand us the URL; all clips mix into one AAC track.",
"type": "object",
"properties": {
"url": {
"description": "URL of the voice clip (any TTS output; mp3/wav/ogg/m4a).",
"type": "string"
},
"start_secs": {
"description": "Seconds from video t=0 where this line starts. Default 0.",
"type": "number",
"format": "double",
"default": 0
},
"volume": {
"description": "Linear multiplier 0..=2. Default 1.",
"type": "number",
"format": "float",
"default": 1
}
},
"required": [
"url"
]
}
}
}
search
Search the web across Baidu/Bing/Sogou/WeChat/Google (aggregated + deduped) and optionally fetch the top results' full content. Use when the agent needs to FIND information online - replaces a search API. Supports image search returning direct image URLs. Optional engines: ["baidu"]-style filter by engine name (invalid names error with the valid list; /doctor lists them with live health). Optional time_range day/week/month/year for news freshness (engines without dated results ignore it). Response carries engine_errors explaining any engine that contributed nothing (CAPTCHA suspension, transient failure).
Parameters7
q
string
required
Search query
fetch_top
integer
optional
Fetch content for top N results
categories
string
optional
Search categories (default: general)
max_results
integer
optional
Maximum number of results (default: 10)
max_chars_per
integer
optional
Max characters per result content
engines
array
optional
Restrict to these engine names (e.g. ["baidu"], ["sogou_wechat"]).
Empty = all engines serving `categories`. Invalid names return an
error listing the valid ones.
time_range
string | null
optional
Freshness window: "day" | "week" | "month" | "year". Honored by
engines with dated results (e.g. bing_news filters by pubDate);
others ignore it.
One-call risk-control report: did this session hit an anti-bot wall? Taobao/tmall's x5 risk control answers 200 like a normal response — either a redirect onto a punish page (_____tmd_____/punish, punish.taobao.com) or an MTop API body carrying FAIL_SYS_USER_VALIDATE / RGV587 / x5secdata. Returns {total, events:[{url,method,status,kind,via}]} where via says whether the wall was navigated into ("url") or swallowed by an API response ("body"). When there are hits, the response also carries the account name (which identity got walled) and a `handoff` instruction: the engine detects and surfaces but does not auto-bypass — a human opens the live view (web/live.html), solves the challenge in this session, and the retry rides the cookie that solving sets. Detection only; no automated solving or bypass.
Click an interactive element by its index (from session_state output) inside a live browser session: scrolls it into view and fires a DOM click on the session's current page. A submit click may navigate the session — the returned `url`/`text_after` reflect the page after the action, and session state (cookies, localStorage, globals) persists for follow-up calls. Indexes come from the most recent session_state; re-list after navigation.
Click at viewport coordinates (CSS pixels) via real mouse events — pointerdown/mousedown, pointerup/mouseup, then click on whatever element is hit there. For canvas/map surfaces with no DOM element to index. click_count 2 adds dblclick.
Derive a new browser session from a live one, carrying the full login state: cookies, localStorage/sessionStorage, viewport pin, dialog policy, proxy and keepalive flags. The source session stays untouched. Use to snapshot a logged-in state before risky actions, or to run the same login in parallel tabs. Returns {session_id (new), cloned_from, url, viewport}.
Parameters1
session_id
string
required
Session ID to derive from (stays alive and untouched)
Raw schema
{
"type": "object",
"properties": {
"session_id": {
"description": "Session ID to derive from (stays alive and untouched)",
"type": "string"
}
},
"required": [
"session_id"
],
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
session_close
Close a browser session and free its resources. For a persistent session this also drops the on-disk login snapshot - idle expiry keeps it, an explicit close does not.
Read the session's recent page console output (log/info/warn/error) as {url, total, matched, messages:[{ts_ms, level, text, url}]}, newest last. Ring buffer of 500 entries; captures output from page scripts, clicks, evals and navigation alike. Optional filters: level (exact, e.g. "error"), since_ts (epoch ms), url_contains (page URL substring), limit (most recent N matches). The fastest way to see WHY a page misbehaves: click the button, call this, read the error.
Parameters5
session_id
string
required
Session ID
level
string | null
optional
Only entries at this level: "log" | "info" | "warn" | "error"
since_ts
integer | null
optional
Only entries logged at or after this Unix epoch millisecond timestamp
url_contains
string | null
optional
Only entries whose page URL contains this substring
Export the session's current cookies as ["name=value", ...] for the page's URL. Use to persist a logged-in session and replay it later via session_create with cookies. Round-trips with session_create's cookies field.
Create a persistent interactive browser session for multi-step interaction - clicking, typing, scrolling, reading state across page transitions. Use when the agent must INTERACT with a page (login flows, forms, pagination, click-through) rather than read it once. Returns session_id; persists 8 min idle. With persistent:true the login state survives idle eviction and server restarts - the same session_id revives logged-in.
Parameters11
url
string | null
optional
Initial URL to navigate to (optional)
use_proxy
boolean
optional
Route through proxy (default: false)
cookies
array
optional
Cookies to inject before navigation: `"name=value"` strings or
CDP-style objects `{"name","value","domain",...}`. Lets the session
start already logged-in. Round-trips with session_cookies.
storage
any
optional
Web Storage to inject after the initial navigation lands:
{"local_storage": {"k":"v"}, "session_storage": {"k":"v"}}. For login
states that live in localStorage rather than the cookie jar.
Round-trips with session_storage.
ttl_secs
integer | null
optional
Idle time-to-live in seconds before the session is evicted
(default: 480, clamped 60..3600). Raise it for long workflows.
width
integer | null
optional
Initial viewport width in CSS pixels. Pinned for the session's life
(survives navigation) so element rects and media queries anchor to
the same layout across every page of the visit.
height
integer | null
optional
Initial viewport height in CSS pixels.
mobile
boolean
optional
Mobile device emulation (coarse pointer, no hover) for the initial
viewport.
keepalive
boolean
optional
Exempt the session from the idle reaper: it lives until
session_close or server exit, so a workflow interrupted by long
non-browser steps keeps its login state.
persistent
boolean
optional
Persist the login state (cookies + localStorage/sessionStorage +
viewport + dialog policy) to the server's local store after every
action. If the session idles out — or the whole server restarts —
the next call with the same session_id revives it logged-in
(storageState-style recovery, no re-login). Explicit session_close
drops the snapshot.
account
string | null
optional
Run as a named login identity (the multi-account layer): a private
cookie jar seeded from the account record, write-back to the account
store after every action. Concurrent logins (`taobao-scraper` vs
`taobao-publisher`) never clobber each other. The account record
survives the session — a later create with the same name picks up
the warm jar. 1-64 chars of [a-zA-Z0-9_-].
Raw schema
{
"type": "object",
"properties": {
"url": {
"description": "Initial URL to navigate to (optional)",
"type": [
"string",
"null"
],
"default": null
},
"use_proxy": {
"description": "Route through proxy (default: false)",
"type": "boolean",
"default": false
},
"cookies": {
"description": "Cookies to inject before navigation: `\"name=value\"` strings or\nCDP-style objects `{\"name\",\"value\",\"domain\",...}`. Lets the session\nstart already logged-in. Round-trips with session_cookies.",
"type": "array",
"items": {
"type": "string"
},
"default": []
},
"storage": {
"description": "Web Storage to inject after the initial navigation lands:\n{\"local_storage\": {\"k\":\"v\"}, \"session_storage\": {\"k\":\"v\"}}. For login\nstates that live in localStorage rather than the cookie jar.\nRound-trips with session_storage."
},
"ttl_secs": {
"description": "Idle time-to-live in seconds before the session is evicted\n(default: 480, clamped 60..3600). Raise it for long workflows.",
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 0
},
"width": {
"description": "Initial viewport width in CSS pixels. Pinned for the session's life\n(survives navigation) so element rects and media queries anchor to\nthe same layout across every page of the visit.",
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0
},
"height": {
"description": "Initial viewport height in CSS pixels.",
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0
},
"mobile": {
"description": "Mobile device emulation (coarse pointer, no hover) for the initial\nviewport.",
"type": "boolean",
"default": false
},
"keepalive": {
"description": "Exempt the session from the idle reaper: it lives until\nsession_close or server exit, so a workflow interrupted by long\nnon-browser steps keeps its login state.",
"type": "boolean",
"default": false
},
"persistent": {
"description": "Persist the login state (cookies + localStorage/sessionStorage +\nviewport + dialog policy) to the server's local store after every\naction. If the session idles out — or the whole server restarts —\nthe next call with the same session_id revives it logged-in\n(storageState-style recovery, no re-login). Explicit session_close\ndrops the snapshot.",
"type": "boolean",
"default": false
},
"account": {
"description": "Run as a named login identity (the multi-account layer): a private\ncookie jar seeded from the account record, write-back to the account\nstore after every action. Concurrent logins (`taobao-scraper` vs\n`taobao-publisher`) never clobber each other. The account record\nsurvives the session — a later create with the same name picks up\nthe warm jar. 1-64 chars of [a-zA-Z0-9_-].",
"type": [
"string",
"null"
],
"default": null
}
},
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
session_dialog
Inspect or flip the session's dialog policy for window.alert/confirm/prompt. Dialogs never block the page: each is auto-answered (default dismiss) and logged into session_console at level "dialog". action "list" reports {policy, prompt_text, dialogs}; "accept" makes subsequent confirm() true and prompt() return prompt_text (or the call's default argument); "dismiss" restores the default.
Parameters3
session_id
string
required
Session ID
action
string
required
"list" reports the policy and dialog history; "accept"/"dismiss" set
the answer applied to subsequent window.confirm/prompt calls (alert
is always logged, never blocking).
prompt_text
string | null
optional
With action "accept": text window.prompt returns once accepted
(omitted keeps the current text).
Raw schema
{
"type": "object",
"properties": {
"session_id": {
"description": "Session ID",
"type": "string"
},
"action": {
"description": "\"list\" reports the policy and dialog history; \"accept\"/\"dismiss\" set\nthe answer applied to subsequent window.confirm/prompt calls (alert\nis always logged, never blocking).",
"type": "string"
},
"prompt_text": {
"description": "With action \"accept\": text window.prompt returns once accepted\n(omitted keeps the current text).",
"type": [
"string",
"null"
],
"default": null
}
},
"required": [
"session_id",
"action"
],
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
session_drag
Drag the mouse from one viewport position to another: press at `from`, `steps` interpolated mousemove events (delay_ms apart), release at `to`. Moves AMarker-style drag targets and canvas selections that only track while the pointer travels.
Parameters5
session_id
string
required
Session ID
from
any
required
Where to press the mouse button down
to
any
required
Where to release it
steps
integer | null
optional
Interpolated mousemove events between from and to (default 10)
delay_ms
integer | null
optional
Delay between moves in ms (default 30) — gives mousemove-driven
widgets time to react per step
Raw schema
{
"type": "object",
"properties": {
"session_id": {
"description": "Session ID",
"type": "string"
},
"from": {
"description": "Where to press the mouse button down",
"$ref": "#/$defs/SessionXy"
},
"to": {
"description": "Where to release it",
"$ref": "#/$defs/SessionXy"
},
"steps": {
"description": "Interpolated mousemove events between from and to (default 10)",
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0,
"default": null
},
"delay_ms": {
"description": "Delay between moves in ms (default 30) — gives mousemove-driven\nwidgets time to react per step",
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 0,
"default": null
}
},
"required": [
"session_id",
"from",
"to"
],
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"SessionXy": {
"type": "object",
"properties": {
"x": {
"description": "X coordinate in viewport CSS pixels",
"type": "number",
"format": "double"
},
"y": {
"description": "Y coordinate in viewport CSS pixels",
"type": "number",
"format": "double"
}
},
"required": [
"x",
"y"
]
}
}
}
session_eval
Execute arbitrary JavaScript in a live browser session and return the result. Runs in the session's current page, so DOM mutations, globals and storage persist across calls — unlike the stateless eval tool, which loads its own throwaway page each call. Script-driven navigation moves the session's URL. JS exceptions are reported with name, line/column and stack.
Parameters3
session_id
string
required
Session ID
script
string
required
JavaScript code to execute
timeout_ms
integer | null
optional
Await budget for the script's promise in ms (default 5000, clamped
100..120000). Pass a larger budget for slow page-side work such as
uploads through the page's own fetch; on expiry the tool errors with
EVAL_TIMEOUT (the script may still be running) instead of returning
a null result.
Raw schema
{
"type": "object",
"properties": {
"session_id": {
"description": "Session ID",
"type": "string"
},
"script": {
"description": "JavaScript code to execute",
"type": "string"
},
"timeout_ms": {
"description": "Await budget for the script's promise in ms (default 5000, clamped\n100..120000). Pass a larger budget for slow page-side work such as\nuploads through the page's own fetch; on expiry the tool errors with\nEVAL_TIMEOUT (the script may still be running) instead of returning\na null result.",
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 0,
"default": null
}
},
"required": [
"session_id",
"script"
],
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
session_export
Export a browser session's recorded action log. Format "bash" (default) returns a runnable curl script that replays every recorded action (navigate/click/input/scroll/eval) against a fresh session on this server — hand it to a shell or cron, zero model tokens. Format "jsonl" returns the raw action log, one JSON object per line. Format "json" returns a flow.json document — the same recording as editable ops ({op, args}) with cookies/storage stripped — that flow_run replays server-side.
Parameters2
session_id
string
required
Session ID
format
string | null
optional
Output format: "bash" (default) renders a runnable curl script that
replays every recorded action against a fresh session; "jsonl" returns
the raw action log, one JSON object per line; "json" returns a
flow.json document (editable ops, cookies/storage stripped) for
replay via flow_run
Raw schema
{
"type": "object",
"properties": {
"session_id": {
"description": "Session ID",
"type": "string"
},
"format": {
"description": "Output format: \"bash\" (default) renders a runnable curl script that\nreplays every recorded action against a fresh session; \"jsonl\" returns\nthe raw action log, one JSON object per line; \"json\" returns a\nflow.json document (editable ops, cookies/storage stripped) for\nreplay via flow_run",
"type": [
"string",
"null"
],
"default": null
}
},
"required": [
"session_id"
],
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
session_input
Type text into an input/textarea element by its index (from session_state output).
Parameters4
session_id
string
required
Session ID
index
integer
required
Element index (from /state output)
text
string
required
Text to type into the input field
events
string | null
optional
Event fidelity: "full" types one character at a time with a
keydown/keypress/input/keyup cycle per character, for pages whose
listeners key on keyboard events (e.g. keypress-Enter login forms).
Default fires a single input+change pair after the value is set.
Raw schema
{
"type": "object",
"properties": {
"session_id": {
"description": "Session ID",
"type": "string"
},
"index": {
"description": "Element index (from /state output)",
"type": "integer",
"format": "uint",
"minimum": 0
},
"text": {
"description": "Text to type into the input field",
"type": "string"
},
"events": {
"description": "Event fidelity: \"full\" types one character at a time with a\nkeydown/keypress/input/keyup cycle per character, for pages whose\nlisteners key on keyboard events (e.g. keypress-Enter login forms).\nDefault fires a single input+change pair after the value is set.",
"type": [
"string",
"null"
],
"default": null
}
},
"required": [
"session_id",
"index",
"text"
],
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
session_list
List live browser sessions with idle age and the time left before auto-eviction. Use to discover a session to reuse instead of creating a new one; sessions expire after 8 min idle.
Read the session's network request log. filter="media" extracts playback/stream URLs (m3u8/HLS, mp4, dash, flv...) actually requested by the page's player at runtime - the reliable way to get a real video link, since links embedded in page HTML are often decoys. Media elements and player iframes the engine never fetches (video/audio/source/iframe src) are merged in as candidates: via="network" entries are confirmed requests, via="dom" entries are candidates carrying their tag (iframes = kind "iframe", navigate into them to sniff). Default returns every request as compact rows (method/url/status/type/size). Navigate to the video page first, let it load, then call this.
Parameters5
session_id
string
required
Session ID
filter
string | null
optional
"media" extracts playback/stream links (m3u8/HLS, mp4, dash, ...) from the requests the page actually issued - the reliable way to get a real video link, since URLs embedded in page HTML are often decoys. Media elements and player iframes the engine never fetches (video/audio/source src, iframe src) are merged in as candidates: entries carry via="network" (confirmed requests) or via="dom" (candidates, with their tag; iframes surface as kind "iframe" - player pages to navigate or sniff inside, not playable URLs). Omit to list every request as compact rows.
include_bodies
boolean | null
optional
Add an `xhr` array of background API responses (the page's own fetch/XHR
traffic with retained bodies) alongside the request rows — the page's
API face is often the cleanest structured read of its data.
url_contains
string | null
optional
Narrow the `xhr` array to URLs containing this substring.
body_max_chars
integer | null
optional
Per-body character cap for the `xhr` array (default 4000).
Raw schema
{
"type": "object",
"properties": {
"session_id": {
"description": "Session ID",
"type": "string"
},
"filter": {
"description": "\"media\" extracts playback/stream links (m3u8/HLS, mp4, dash, ...) from the requests the page actually issued - the reliable way to get a real video link, since URLs embedded in page HTML are often decoys. Media elements and player iframes the engine never fetches (video/audio/source src, iframe src) are merged in as candidates: entries carry via=\"network\" (confirmed requests) or via=\"dom\" (candidates, with their tag; iframes surface as kind \"iframe\" - player pages to navigate or sniff inside, not playable URLs). Omit to list every request as compact rows.",
"type": [
"string",
"null"
],
"default": null
},
"include_bodies": {
"description": "Add an `xhr` array of background API responses (the page's own fetch/XHR\ntraffic with retained bodies) alongside the request rows — the page's\nAPI face is often the cleanest structured read of its data.",
"type": [
"boolean",
"null"
],
"default": null
},
"url_contains": {
"description": "Narrow the `xhr` array to URLs containing this substring.",
"type": [
"string",
"null"
],
"default": null
},
"body_max_chars": {
"description": "Per-body character cap for the `xhr` array (default 4000).",
"type": [
"integer",
"null"
],
"format": "uint",
"minimum": 0,
"default": null
}
},
"required": [
"session_id"
],
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
session_screenshot
Screenshot the session's CURRENT DOM state (mutations from clicks/evals included) as a base64 PNG via the built-in renderer. Width/height default to the session's viewport, so session_viewport + session_screenshot shows the responsive layout. Returns {url, width, height, image_base64, format}.
Parameters6
session_id
string
required
Session ID
width
integer | null
optional
Render width in CSS pixels; defaults to the session's current viewport
height
integer | null
optional
Render height in CSS pixels; defaults to the session's current viewport
full_page
boolean
optional
Capture the full scrollable page instead of the viewport (default: false)
selector
string | null
optional
CSS selector: capture only that element's box
selector_all
boolean
optional
With selector, capture every match (default: first match only)
Raw schema
{
"type": "object",
"properties": {
"session_id": {
"description": "Session ID",
"type": "string"
},
"width": {
"description": "Render width in CSS pixels; defaults to the session's current viewport",
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0
},
"height": {
"description": "Render height in CSS pixels; defaults to the session's current viewport",
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0
},
"full_page": {
"description": "Capture the full scrollable page instead of the viewport (default: false)",
"type": "boolean",
"default": false
},
"selector": {
"description": "CSS selector: capture only that element's box",
"type": [
"string",
"null"
]
},
"selector_all": {
"description": "With selector, capture every match (default: first match only)",
"type": "boolean",
"default": false
}
},
"required": [
"session_id"
],
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
session_scroll
Scroll the page up or down by a number of viewport-heights.
Select files on a file input programmatically (Playwright setInputFiles semantics): builds File objects from base64 content, assigns them to input.files, then dispatches input+change so framework onChange handlers fire. Selector-addressed because file inputs are often hidden and absent from the session_state index.
Parameters3
session_id
string
required
Session ID
selector
string
required
CSS selector for the file input, e.g. "input[type=file]". File inputs
are often hidden, so this is selector-addressed rather than using the
/state index.
files
array
required
Files to select
Raw schema
{
"type": "object",
"properties": {
"session_id": {
"description": "Session ID",
"type": "string"
},
"selector": {
"description": "CSS selector for the file input, e.g. \"input[type=file]\". File inputs\nare often hidden, so this is selector-addressed rather than using the\n/state index.",
"type": "string"
},
"files": {
"description": "Files to select",
"type": "array",
"items": {
"$ref": "#/$defs/SessionFileSpecParams"
}
}
},
"required": [
"session_id",
"selector",
"files"
],
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"SessionFileSpecParams": {
"type": "object",
"properties": {
"name": {
"description": "File name the page sees (and what multipart uploads as filename)",
"type": "string"
},
"content_base64": {
"description": "File content, standard base64 (padding allowed)",
"type": "string"
},
"mime_type": {
"description": "MIME type (default \"application/octet-stream\")",
"type": [
"string",
"null"
],
"default": null
},
"last_modified": {
"description": "Last-modified time in ms since epoch (default: now)",
"type": [
"number",
"null"
],
"format": "double",
"default": null
}
},
"required": [
"name",
"content_base64"
]
}
}
}
session_state
Get the current page state as an indexed list of interactive elements. Returns compact text with [N] indexes for use with click/input tools.
Snapshot the session's localStorage/sessionStorage for the current origin: {url, local_storage, session_storage}. Feed it back via session_create's `storage` field to restore a logged-in state in a new session — the half of login state that cookies can't carry (many sites keep the session token in localStorage). Call before the session idles out.
Set the session's viewport (device emulation): scripts see innerWidth/innerHeight move, media queries like (max-width: 600px) re-evaluate, element rects re-anchor, and mobile=true flips pointer/hover matchMedia answers to coarse/none. Omitted width/height keeps the current value.
Parameters4
session_id
string
required
Session ID
width
integer | null
optional
Viewport width in CSS pixels; omit to keep the current width
height
integer | null
optional
Viewport height in CSS pixels; omit to keep the current height
mobile
boolean
optional
Mobile emulation: matchMedia answers pointer:coarse / hover:none and
navigator.maxTouchPoints reports 5 (default: false)
Wait until a CSS selector matches or a JS predicate turns truthy, with a timeout. The page's event loop keeps running while waiting (fetches, timers, promise chains progress), so this replaces blind sleeps for async content: navigate, session_wait for '.price-card', then click/read. Returns {matched, elapsed_ms, detail:{tag,text} or the predicate value}; errors with `timeout ...` naming the selector/predicate on expiry. Exactly one of selector/predicate.
Parameters4
session_id
string
required
Session ID
selector
string | null
optional
CSS selector to wait for (e.g. ".price-card")
predicate
string | null
optional
JS expression polled until truthy (e.g. "document.querySelectorAll('.card').length >= 3")
timeout_ms
integer
optional
Give up after this many milliseconds (default: 10000, max: 120000)
A browser built for agents from the first line of code — not a human browser bolted onto automation. See the world, read it, search it, act on it, and keep what you read: one Rust binary with built-in V8, no Chromium required.
Humans have Chrome. Agents have AginxBrowser.
One binary, zero dependencies, instant service. HTTP API + native MCP + CDP — agents plug in and go, and existing Playwright / Puppeteer / browser-use code attaches directly.
The star that got our attention: Pierre Tachoire, co-founder of Lightpanda — the headless browser our bench measures against — starred the repo. 90 seconds on why that mattered to us.
Real pages rendered by AginxBrowser's diting engine (no Chromium) — Wikipedia, this repo, Rust. Screenshot it yourself →
Why Agents Need Their Own Browser
Measured against headless Chrome on the same 20 pages, same network (bench, 2026-08-28): 7.6× faster to agent-usable text (p50 532 ms vs 4 053 ms), ~10× less memory (227 MB for the whole process vs ~2.1 GB per Chrome page), and 0 hard failures where Chrome's --dump-dom produced no DOM on 5 of 40 loads. An agent's total cost is browser efficiency × model efficiency — this is the browser half.
Existing "browser automation" was built for humans or for one-shot scraping — not for agents:
AginxBrowser
Puppeteer/Playwright
Firecrawl
Browser-use
Designed for
Agents first
Human debugging
Scraping service
LLM wrapper
Dependencies
Single binary, no Chromium
Chromium ~500MB
Docker ~1GB
Chromium
Sees (screenshots)
✅ built-in diting rendering engine
Needs Chromium
❌
Needs Chromium
Reads
markdown + js_extract + fetch receipts
DIY
markdown
DIY
Writes documents
✅ render_markdown: deterministic HTML + inline-SVG diagrams
❌
❌
❌
Finds (search)
✅ 15 engines, 7 categories, merged
❌
❌
❌
Acts
indexed session interaction
DevTools API
❌
LLM-driven
Remembers
✅ local fetch/search cache (SQLite FTS5)
❌
crawl cache
❌
Protocol
HTTP + native MCP + CDP
Node API
HTTP
Python
TLS fingerprints
✅ Chrome/Firefox/Safari/Edge
Plugin required
❌
❌
CAPTCHA
✅ detect + auto-wait + optional 2captcha
DIY
❌
❌
Interactive sessions
✅ persistent
✅
❌
✅
An agent needs five things from a browser: see, read, find, act, remember. One binary covers them all — systemd-friendly, MCP-native for Claude/Cursor, zero dependencies.
Core advantage: no Chromium. AginxBrowser inlines a full browser engine (V8 + Rust HTTP stack + the diting CSS/layout/paint rendering engine, with the Blitz/Stylo/Taffy lineage as its reference implementation). No Puppeteer, no Chrome, no Docker. One Rust binary under systemd is your agent browsing infrastructure.
Three Things Stateless Renderers Can't Do
Most new "agent browsers" are stateless, fingerprint-less one-shot renderers — fine for public pages, dead on arrival against Cloudflare or login flows. AginxBrowser goes the opposite way:
🔐 Real TLS fingerprints — stealth mode replicates the complete Chrome145 / Firefox133 / Safari / Edge TLS handshakes via BoringSSL (not just a UA string), switchable per request; Cloudflare Turnstile challenges wait automatically for cf_clearance. Fingerprint-less engines eat 403s — we get through.
🤝 Stateful interactive sessions — login state injectable and exportable (session_create(cookies=...) ↔ session_cookies), surviving pagination and multi-step flows; persistent: true even survives idle eviction and server restarts — the same session id comes back logged in. One-shot engines throw state away.
🔌 MCP native — 37 tools as first-class citizens (not a CDP shim). Claude Code / Cursor / Claude Desktop connect in one line. HTTP + MCP dual protocol — plus a CDP bridge, so the DevTools ecosystem works too.
Reference point: Cloudflare's Kitesurf explicitly ships neither real TLS-fingerprint negotiation nor persistent auth sessions — anti-bot and login territory is exactly where AginxBrowser plays.
Apache-2.0 open source, single binary — self-host today, no cloud lock-in.
Every Fetch Is a Receipt
Agents act on what a browser tells them, so the response reports what actually happened — not just "got a 200":
tier — which path served the page: plain HTTP (~100 ms) or the V8-rendered browser tier. An agent can see why a fetch was fast or slow.
redirected_from — the full redirect trail. redirected_from[0] is the URL you asked for, url is where the content actually came from — requested paired with effective, every hop visible.
content_hash + changed_since_prev — every fetch is hashed; consecutive samples of the same URL can be diffed. A rate-limited origin serving the same frozen 200 body for days reads as changed_since_prev: false — the cheapest drift detector there is.
captcha_event — when a challenge page was detected (and solved, if a solver is configured), the response says so instead of handing over a challenge page as if it were content.
The local cache builds on the same idea: search hits come back with [§ heading] section prefixes so an agent knows where on the page a hit landed, and ranking fuses keyword relevance with freshness.
Capabilities
Tiered rendering: static pages over plain HTTP (~100ms); V8 spins up only when JS rendering is needed (~1-2s) — 90% of the bench page set served without spinning up V8 at all; every response reports which tier served it (tier field)
Multi-engine meta-search: general web (Baidu / Bing / Sogou / WeChat / Google / DuckDuckGo), news (Bing News), code (Stack Overflow, GitHub), packages (npm, PyPI), academic (arXiv), AI models (Hugging Face) — 15 engines across 7 categories, queried concurrently, merged and deduplicated. Operators can plug a private Meilisearch index into the same /search. Search → read in one step
Image search: categories=images hits Baidu/Bing image indexes and returns direct binary image_url links (downloadable straight to jpg/png) plus source_url provenance
Interactive sessions: persistent browser sessions with indexed interaction (state/click/input/scroll/eval) — agents browse like humans do, and session_export turns what an agent figured out into a runnable curl replay script (zero model tokens on re-run) — or, with format=json, into a flow document (flow_run replays it server-side with {{var}} substitution, wait/expect gates and saved outputs; installed flows live in workflow/<name>/flow.json, dropped in without a rebuild). Session tools also cover the acting part: session_viewport simulates device viewports (media queries respond), session_wait blocks on a selector or predicate with a timeout, session_screenshot renders the live state, session_console replays the page's console ring, and session_storage exports/restores cookies plus localStorage for login hand-off
Playback-link sniffer: session_network(filter=media) extracts the m3u8/mp4/dash URLs a page's player actually requested at runtime — links found only in page HTML are often decoys, so the request log is the source of truth. GET /session/{id}/har exports the same traffic as HAR 1.2 (retained bodies included)
CDP bridge: /json/version + /devtools/{kind}/{id} WebSocket — chromium.connectOverCDP() from Playwright, Puppeteer, or browser-use attaches with one line, and agent-browser drives it via --cdp (snapshot returns the synthesized accessibility tree with @ref handles) (integration guide). DevTools ecosystem compatibility without becoming a CDP shim
File download: streaming to disk (no memory buffering), SHA-256 integrity, resume of interrupted transfers — for binaries, archives, datasets
Local cache that remembers: every fetch/search lands in SQLite (FTS5) at ~/.aginxbrowser/cache.db. The cache tool re-answers from what the agent already read instead of re-paying network time: full-text search with CJK substring matching, keyword × freshness fusion ranking, [§ heading] section-aware snippets, per-URL content hashes for drift detection, TTL-bounded, per-session scoping for shared deployments
CAPTCHA handling: type detection with automatic Cloudflare challenge wait and optional 2captcha integration — search never stalls on verification pages
JS data extraction: js_extract pulls window.__INITIAL_STATE__ and other structured data out of SPAs
Document generation: render_markdown turns markdown into a deterministic, self-contained HTML artifact — the document layer, so agents never write HTML by hand. Prose rides a plain offline shell (no fonts, no scripts); fenced archify blocks carry typed zero-coordinate diagram JSON (sequence / workflow / architecture / dataflow / lifecycle families) and render to inline SVG via the layout engine. Same input, same bytes — the receipt carries the sha256 so determinism is verifiable. theme (light/dark) and preset (classic / signal-flow / blueprint / editorial) bake colors at generation time; quality: "showcase" is the delivery gate, grading route crossings, label clearance and rhythm without touching the artifact bytes. Guided-view tabs plus window.agxViewer (focus / ego / route / reach) make the artifact interactive; with session_id it loads into a live session and the reply grades how it fits the viewport (fits/tall/wide/oversized). Mermaid sources are the agent's job to translate into archify JSON, not the engine's. Diagram vocabulary adapted from archify (MIT)
Screenshot rendering: /screenshot endpoint (opt-in --features screenshot) paints the JS-rendered DOM with the diting rendering engine — pure CPU, no Chromium — to PNG. Vision input for agents
Timeline video: /video endpoint + render_video MCP tool render a page's animation timelines to MP4 — the page's scripts register GSAP-style timelines in window.__timelines (duration() + pause(t)), each frame seeks to t=i/fps and paints the viewport, and the frames pipe into ffmpeg (H.264, yuv420p). Deterministic by construction: no wall clock in the pixel values, same render twice = same MP4. Needs ffmpeg on PATH
Page set (PDF/PNG/PPTX/DOCX): /pdf endpoint + render_pdf MCP tool cut a rendered page into pages and package them — print mode paginates at top-level block boundaries (default A4 @96dpi, no half-cut text where a break can land on a block edge), slides mode makes one page per CSS-selector match sized to the element (an HTML deck with one .slide per page exports as a real deck). Image-based PDF: per-page JPEG via DCTDecode, hand-rolled PDF 1.4 writer, zero new dependencies. PPTX packages the same pages as one slide per page; DOCX as one page-sized section per page — both hand-rolled OOXML (stored-ZIP writer, fixed timestamps), byte-deterministic, zero new dependencies
TLS fingerprint spoofing: stealth mode impersonates Chrome145/Firefox133/Safari/Edge, switchable per request
MCP server: --mcp mode exposes 37 tools (fetch/eval/search/download/cache + session + flow + screenshot + video/pdf + docgen tools) — Claude Code / Claude Desktop / Cursor call them directly
Firecrawl compatible: /v1/scrape endpoint — existing Firecrawl clients migrate by changing the base URL
DNS rebinding protection: built-in SSRF guard + post-resolution IP validation
A Browser, Not a Crawler
AginxBrowser exists for real-time retrieval: an agent arrives with a question, reads a handful of pages, leaves with the answer. It is not a crawling tool — and the product is shaped so it can't quietly become one:
robots.txt is not our gate. The RFC 9309 checker ships built in, but a real-time lookup layer isn't a crawler and doesn't do crawler etiquette by default; operators who want it set AGINXBROWSER_HONOR_ROBOTS=1.
No site-walking API. There is no crawl endpoint and no link-following recursion — every page load happens because an agent asked for that page.
Built-in budgets. Per-domain: 20 pages/minute. Per interactive session: 200 pages. Toggled via AGINXBROWSER_DOMAIN_RATE_PER_MIN / AGINXBROWSER_SESSION_PAGE_LIMIT (0 disables on your own instance). Generous for an agent grinding through docs or a console; fatal to the page-after-page crawl pattern, including subdomain rotation (one registrable domain, one budget).
The hosted instance (browser.aginx.net) runs tighter budgets. Every user shares one egress IP, and keeping sites comfortable with that IP is part of the service. Self-host if you want different numbers.
Need to bulk-crawl a site? Use a crawler. This isn't one, and it won't become one.
What It's For
Not demos — real jobs agent browsers are doing today:
Grind through admin consoles — AWS / App Store Connect / Google Play, dozens of menu layers per task. Let the agent click; it comes back only when authorization is needed.
Batch actions behind login — fill carts, dig through order history, check pages that only render while logged in. Inject cookies, operate, export for reuse.
Past anti-bot walls — Cloudflare protection, Turnstile challenges, TLS fingerprint checks. Stealth mode pushes through instead of retreating at 403.
The Chinese internet — Baidu / Sogou / WeChat meta-search across 5 engines, correct Chinese page rendering. Not English-web-only.
Multimodal vision — screenshots as visual input for look-and-judge flows: picking seats, recognizing layouts, verifying rendering.
Where It Sits in the Computer-Use Stack
Computer-use agents come in two layers. GUI-layer stacks (Cua, desktop CUA agents) drive a whole machine: screenshots of a display in, X11 mouse/keyboard events out, a VM or container per session. Engine-layer browsers skip the desktop — the page itself is the machine. AginxBrowser is the engine layer:
GUI layer (desktop CUA)
AginxBrowser (engine layer)
Action space
screen pixels → OS input events
DOM/CDP: click by coordinates or selector, real event dispatch
State readout
screenshot of a display
structured DOM / accessibility tree, screenshot optional
Per-session cost
Xvfb + VM/container
one process, no display server
Frames
flattened into pixels
first-class: hit-testing descends into iframes, frame-scoped evaluate
Replay
best-effort (compositor timing)
action logs export as runnable curl / flow JSON
Validated against the GUI layer's own harness: the cua-bench webtop gym — a window manager where every "window" is an iframe — runs on AginxBrowser through the CDP bridge (click task: PASS end-to-end). That's the path that exercises iframe hit-testing, frame-scoped script state, and coordinate translation, which is exactly where engine-layer substitutes usually break. browser-use attaches the same way Playwright does: point cdp_url at the bridge.
# Download -> inspect the contents -> run only after review (never blind-run network scripts)
curl -fsSL https://raw.githubusercontent.com/yinnho/aginxbrowser/main/skill.sh -o skill.sh
less skill.sh
bash skill.sh
Register MCP only:
bash
claude mcp add aginxbrowser --transport http https://browser.aginx.net/mcp
Or just hand this block to your AI agent — it sets itself up:
text
Set up aginxbrowser as your web access layer:
1. Register the MCP server (skip if already present):
claude mcp add aginxbrowser --transport http https://browser.aginx.net/mcp
No claude CLI? The HTTP API works directly — no MCP needed.
2. Verify capabilities at https://browser.aginx.net/doctor before calling
feature-gated endpoints (screenshot/stealth must be listed).
3. From now on, route web tasks through it instead of raw curl:
- read a page / scrape a URL -> fetch {url} (markdown output, Cloudflare bypassed by default)
- search the web -> search {q, fetch_top:3} (5 engines incl. Baidu/WeChat)
- see a page -> screenshot {url}
- login / form / click-through-> session_create -> session_state -> session_input/session_click -> session_close
Self-hosting:
bash
# macOS / Linux via Homebrew
brew install yinnho/aginxbrowser/aginxbrowser
aginxbrowser doctor # features + fonts + egress self-check# Docker (Docker Hub, mirrored on GHCR)
docker run -p 8089:8089 yinnho/aginxbrowser:latest
# (or ghcr.io/yinnho/aginxbrowser:latest)# Or the prebuilt binary (platform detect + sha256 + mirror fallback + doctor self-check)# macOS / Linux / Windows (git-bash; prebuilt Windows ships from v0.3.1, full `stealth`+`screenshot` feature set from v0.4.0)# Cautious: download -> inspect -> run (never blind-run network scripts)
curl -fsSL https://browser.aginx.net/install.sh -o install.sh
less install.sh && bash install.sh
# Or straight in, if you trust the repo:# curl -fsSL https://browser.aginx.net/install.sh | sh# GitHub slow/blocked? AGINXBROWSER_GH_PROXY=https://ghfast.top/ bash install.sh
aginxbrowser doctor # features + fonts + egress self-check# Or build from source (--features stealth,screenshot or you lose both)
cargo build --release --features stealth,screenshot
# Start the service
./target/release/aginxbrowser
# → Listening on 0.0.0.0:8089# Verify
curl http://127.0.0.1:8089/health
# → {"status":"ok","engine":"diting"}# Fetch a page
curl -sS -X POST http://127.0.0.1:8089/fetch \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com"}'# Search (snippets only by default; fetch_top grabs page bodies for the top N)
curl -sS -X POST http://127.0.0.1:8089/search \
-H "Content-Type: application/json" \
-d '{"q":"macbook price","max_results":5,"fetch_top":2,"max_chars_per":2000}'# Create an interactive session
curl -sS -X POST http://127.0.0.1:8089/session/create \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com"}'# → {"session_id":"s_1","url":"https://example.com/"}# MCP mode (for AI agents)
./target/release/aginxbrowser --mcp
REST Routes
Every capability is plain HTTP — no SDK required. There is no /openapi.json (the routes are few enough to list here); full request/response fields for each route are in docs/API.md.
Run a recorded/edited flow JSON to completion — zero model tokens (name runs an installed workflow/<name>/flow.json, flow is inline, vars substitute {{placeholders}}, session_id composes with imported login state)
POST
/session/create
Start an interactive session (cookies/UA carried across calls)
POST
/import/curl
Create a logged-in session from a DevTools "Copy as cURL"
Two MCP tools have no REST route: cache (query the local fetch/search cache) and render_markdown (markdown → deterministic HTML artifact). Everything an agent does through MCP is callable over REST above.
# Standard build (no stealth; TLS fingerprint features inactive)
cargo build --release
# With stealth (requires go + cmake + C++ toolchain; enables TLS fingerprint spoofing)
cargo build --release --features stealth
# With screenshot rendering (enables /screenshot; adds the rendering stack, +30-40MB)
cargo build --release --features screenshot
# Full featured (recommended for production)
cargo build --release --features stealth,screenshot
Requirements: Rust 1.78+; the V8 static library downloads automatically on first build. The stealth feature additionally needs go, cmake, and a C++ compiler. The screenshot feature ships with a bundled CJK font subset (GB2312 + common symbols) — no system fonts required for correct Chinese rendering.
If your network can't reach the rusty_v8 CDN (build hangs with zero progress after "downloading v8"), pre-fill ~/.cache/rusty_v8 with the librusty_v8.a.gz for your version (fetch it from any reachable mirror/host and gunzip into place) and the build script skips the download.
Runtime Environment Variables
Variable
Default
Description
AGINXBROWSER_BIND
0.0.0.0:8089
Listen address
--cdp-port N (flag)
unset
Bind on 127.0.0.1:N instead — the local agent-tooling entry (agent-browser --cdp N, Playwright connectOverCDP). Beats AGINXBROWSER_BIND when both are set
AGINXBROWSER_STEALTH
enabled
0 disables stealth (for diagnostics)
AGINXBROWSER_UA
Linux Chrome145
Spoofed User-Agent
AGINXBROWSER_ACCEPT_LANGUAGE
zh-CN,zh;q=0.9,en;q=0.8
Accept-Language header
AGINXBROWSER_PROXY
none
Optional fallback proxy. Blocked-source engines (Google, Bing News, Hugging Face) connect directly first and fall through to this proxy only when the direct attempt fails — overseas deployments need no proxy at all; per-request use_proxy:true also routes fetch/search through it. Browser/session/CDP navigations to known-blocked domains (wikipedia.org, github.com, …) route through it automatically. Standard HTTP_PROXY/HTTPS_PROXY/ALL_PROXY are deliberately ignored by the engine (set them for other tools freely); startup logs a warning when it sees one
AGINXBROWSER_NAV_CHAIN_LIMIT
10
JS navigation-chain cap: documents a page may chain via location/form hops before navigation aborts. The count includes the requested document (10 = initial doc + 9 hops). Raise for legit long chains (SSO handover across providers); HTTP 3xx redirects are budgeted separately (20, per Fetch spec / browser parity)
AGINXBROWSER_CACHE_TTL_SECS
600
/fetch cache TTL, 0 disables
AGINXBROWSER_HONOR_ROBOTS
unset
robots.txt is not consulted by default on /fetch, /screenshot, /download and MCP tools; set 1 to opt in (operator choice)
AGINXBROWSER_ALLOW_FILE_ACCESS
unset
Opt in to file:// reads — navigation, subresources, /fetch, and CDP setFileInputFiles. Same as the --allow-file-access CLI flag. Off by default: the server binds 0.0.0.0, so an open gate hands local files to anyone who can reach the port. Set it on a local dev instance, not a hosted one
AGINXBROWSER_ALLOW_PRIVATE_NETWORK
unset
Opt in to loopback/RFC1918/link-local fetches (the SSRF gate). Same as the --allow-private-network CLI flag — dev machines only
AGINXBROWSER_ALLOW_NETWORK
unset
Scoped alternative: comma-separated CIDR allowlist (e.g. 10.20.0.0/16,192.168.1.0/24) that opens just those ranges — cloud-metadata endpoints (169.254.169.254, 100.100.100.200) and everything else stay blocked. Same as --allow-network <cidrs>
AGINXBROWSER_FONT_DIR
unset
Directory of extra fonts (.ttf/.otf/.ttc) loaded as tail fallbacks for scripts the bundled CJK subset doesn't cover (Korean, Thai, Arabic, …). Same as the --font-dir <path> CLI flag. Faces the bundle already covers keep bundle rendering — dir fonts are coverage tails, not named-family overrides
AGINXBROWSER_ROBOTS_TTL_SECS
3600
Per-host robots.txt policy cache TTL
AGINXBROWSER_DOMAIN_RATE_PER_MIN
20
Per-registrable-domain page budget per minute (subdomains share one budget); over-budget requests get 429 with the stance message. 0 disables. See "A Browser, Not a Crawler"
AGINXBROWSER_SESSION_PAGE_LIMIT
200
Total pages one interactive session may walk (navigation-causing clicks count); over-budget navigations are refused, the current page stays interactive. 0 disables
AGINXBROWSER_MCP_ALLOWED_HOSTS
unset
Extra Host values accepted by /mcp (comma-separated) — the transport's DNS-rebinding guard defaults to loopback, so add your LAN IP or Docker hostname when other machines call the instance
AGINXBROWSER_STORE
on
Local fetch/search cache; 0/false/off disables
AGINXBROWSER_STORE_PATH
~/.aginxbrowser/cache.db
SQLite database location (created 0600)
AGINXBROWSER_STORE_TTL_HOURS
720
Cached page TTL
AGINXBROWSER_STORE_SEARCH_TTL_HOURS
168
Cached search-result-set TTL
AGINXBROWSER_STORE_SCOPE
global
session gives each MCP client session its own cache scope — set this on public multi-client deployments
CAPTCHA_SOLVER_API_KEY
none
2captcha API key; enables CAPTCHA auto-solving
CAPTCHA_SOLVER_SERVICE
2captcha
CAPTCHA solving provider
AGINXBROWSER_MEILI_URL
none
Meilisearch base URL; set to enable the private-index engine
AGINXBROWSER_MEILI_INDEX
none
Meilisearch index uid to query
AGINXBROWSER_MEILI_KEY
none
Optional Bearer key for the Meilisearch instance
API Documentation
Full API reference → docs/API.mdCDP integration guide → docs/integrations.md — Playwright / Puppeteer / browser-use one-liners
Security audit notes → docs/skills-sh-audit.md — why skills.sh shows "Critical Risk", and which real product feature each warning corresponds to
AginxBrowser is pure attach-alongside infrastructure — like a real browser, it runs as an independent service that anything can call, without embedding host code or polluting host config. Deploy one instance per machine (under systemd) and every app needing "render + scrape" capability shares it.
Three attach points:
HTTP — /fetch, /search, /screenshot, /download for any language with an HTTP client
MCP — one line into Claude Code / Cursor / Claude Desktop (above)
CDP — point Playwright / Puppeteer / browser-use at ws://your-host:8089/devtools/browser/<id>; agent-browser drives it with --cdp (--cdp-port N binds loopback for that). Google's chrome-devtools-mcp attaches too: aginxbrowser --cdp-port 9223 + chrome-devtools-mcp --browser-url http://127.0.0.1:9223 — its full tool battery (pages, snapshot, evaluate, screenshot, network, resize) passes against the diting engine. See docs/integrations.md
Integration: read the environment variable AGINXBROWSER_URL=http://127.0.0.1:8089. Unset → behavior unchanged; set → risk-controlled sites automatically route through AginxBrowser for rendering, falling back gracefully on failure.
Known Limitations
Screenshots are opt-in: /screenshot requires cargo build --release --features screenshot (adds the diting rendering stack). The default (and only) render engine in that build is diting — our own CSS+layout+paint stack, zero Blitz/Stylo code. The pinned-rev Blitz reference pipeline is a separate opt-in, --features blitz-reference, for comparison renders and the dual-engine cross-check tests. Complex-site CSS is approximate on both (not pixel-perfect like Chromium)
Element coordinates supported: /screenshot with selector returns element page coordinates (selector_rects, CSS px); selector alone crops directly to that element. Inline elements (<a>text</a>) get a rect too on the default diting engine — a union of their flattened inline content, strut-expanded to the element's own line-height like Chrome reports for replaced-only inlines (<a><img></a> → line-box height, not the image height). Empty inlines still have no rect — pick a block ancestor there
JS interaction broadly works; heavy-fingerprint pages may still fail: React/Vue event delegation works normally (URL-reflection attributes like src/href resolve to absolute URLs so Next.js/webpack hydrate and clicks trigger handlers). Heavy-fingerprint auth pages (WorkOS/Cloudflare) probing navigator.plugins, WebGL canvas etc. may still break until stealth fingerprint coverage completes
Proxy support: HTTP/HTTPS/SOCKS5 via AGINXBROWSER_PROXY
Hard risk-controlled sites: Baidu Wenku unsupported; Zhihu articles need a valid __zse_ck
Star History
If AginxBrowser saved you a headless-Chrome fleet or a scraping headache, a star is how other agents (and their humans) find the project.