Repository navigation
v0.3.0
0.3.0 — Stateless REST API + OpenAPI, 11 new tools, per-session state, configurable resources
36 tools (25 → 36). A second HTTP surface — /api/v1/* — exposes the
same tools as plain JSON POST so scripts in any language can drive the same
live browser an MCP agent uses, with structured JSON (not LLM-formatted
text) on the wire. An OpenAPI 3.1 spec is generated from the tool registry;
a Node JS/TS client lives in a new companion workspace package. 11 new MCP
tools, 2 tool extensions, per-session active-tab + snapshot store, several
operational knobs exposed, security hardening, and a small breaking change
to locator defaults. 470 tests across 48 files.
New tools (11)
browser_press— press a key or chord (Enter,Control+A,
Meta+C). Withtarget, focuses that element first; otherwise sends to
the page.browser_hover— hover an element to reveal menus / tooltips /
hover-only controls. Same locator strategies asbrowser_click.browser_select_option— pick option(s) in a native<select>by
value(default), visiblelabel, or zero-basedindex. Works for
<select multiple>.browser_check— set a checkbox / radio idempotently (unlikeclick
which toggles):checked: true|falseensures the target state.browser_drag— HTML5 drag-and-drop. Specifysourceandtarget
with their own locator strategies.browser_fill_form— batch-fill multiple fields in one call (fields
applied in order; each sets exactly one ofvalue/checked/
options). Aborts on the first failing field and reports which.browser_storage— get / set / remove / clear keys in the active
tab'slocalStorageorsessionStorage. localStorage persists in the
named profile.browser_handle_dialog— set policy for the next native dialog
(alert/confirm/prompt) before triggering it. Without this,
dialogs are auto-dismissed.browser_set_geolocation— set emulated coordinates (pair with
browser_permissionsto grantgeolocation).browser_console_log— inspect recent browser console output (ring
buffer of last 500 per profile):console.log/info/warn/error/debugplus
uncaught page errors (levelpageerror). Filter bytab_id,level,
text_regex.browser_network_body— return a captured HTTP response body. Texty /
JSON only, last 50, size-capped. Filter byurl_regex+method;
indexcounts back from the most recent match.
Extended tools (2)
browser_waitgains aconditionmode — polls a JS expression via
page.waitForFunction(mutually exclusive withselector/state).
Lets you wait forwindow.__ready === trueetc.browser_configuregainsextra_headers: Record<string,string>—
merged intocontext.setExtraHTTPHeaders(in addition to internal UA /
Accept-Language).
Stateless REST API (/api/v1)
A second HTTP surface beside /mcp. Every call is an independent POST;
no session handshake. ?profile=<name> selects which shared live browser
to drive — the same profile an MCP agent uses → same cookies, tabs,
and network log. Pass tab_id in the body to target a specific tab.
POST /api/v1/tools/<tool>?profile=<name>— run a tool; body = same JSON
args as the MCP tool.GET /api/v1/tools— discovery (names + descriptions).GET /api/v1/openapi.json— OpenAPI 3.1 spec served live (auth-gated).DELETE /api/v1/profiles/<name>— release a profile holder (the browser
is shut down only if no MCP session still holds it; ref-count).
Response envelope — HTTP status reflects the transport layer only:
Non-2xx is reserved for transport problems: 400 invalid args (zod
issues included) or bad profile, 401 missing/invalid Bearer, 403
Origin not allowed, 404 unknown tool, 415 non-JSON body, 503
session cap reached. The same auth / Origin / Content-Type / body-size
guards and the max_sessions cap apply to /api as to /mcp (shared
gate).
Internally the REST side is sessionless to the caller but keeps one
in-memory profile holder per profile (one entry in the shared sessions
map, keyed rest:<profile>) that reuses the shared BrowserManager and a
per-tool handler cache. The holder is created synchronously on first call
— no await between sessions.get and sessions.set — so two concurrent
first-touch calls can't race-spawn two managers on one profile dir. Reaped
on the same session_ttl as MCP sessions.
OpenAPI 3.1
src/openapi.ts derives the spec from the tool registry using zod v4's
native z.toJSONSchema (one POST path per tool, io: "input" semantics,
$schema dialect marker stripped). Cross-field .refine constraints
(browser_cookies "url OR domain+path", browser_fill_form "exactly one
of value/checked/options") don't serialise to JSON Schema — they live in
each tool's description and are enforced server-side with a 400 on
violation. Per-tool output schemas are not in v1 (the data channel
is generic). The spec is served live and also emitted as a static
apps/browser-mcp/openapi.json artifact (npm run openapi) so clients
can codegen without running the server.
JS/TS client — @graphmemory/browser-client 0.1.0
A new workspace package at packages/browser-client-js/. Zero runtime
dependencies (uses the global fetch, Node 18+). Generic
tool<Name>(name, args, { profile? }) is type-checked end-to-end against
the OpenAPI spec — misspelled tool names and bad arguments fail at compile
time — and ergonomic shortcuts (open / read / click / type /
snapshot / evaluate / tabsList) derive their opts types from the
same schema. listTools(), openapi(), and releaseProfile() round out
the surface. Argument types are generated with openapi-typescript
(devDep only, committed under src/generated/openapi.ts). Tool-level
failures resolve with { ok: false }; transport failures (4xx/5xx) throw
BrowserClientError carrying .status and .body.
Structured data channel on tool handlers
ToolResult gains an optional data?: unknown. Data-returning handlers
now attach the structured object they already compute — browser_read
→ { url, mode, content }; browser_network_log / browser_console_log
→ { entries, total }; browser_tabs_list → { tabs, active };
browser_snapshot → { snapshot } or { diff };
browser_open / back / forward / reload → the TabInfo;
browser_expect → { ok, assertion, expected, actual };
browser_evaluate → { result }; cookies/storage/network-body etc. The
MCP transport ignores the extra field (no behaviour change for agents);
the REST layer returns it as the response data.
Per-session state (active tab + snapshot store)
BrowserManager (per profile, shared) now owns only the shared resources
— BrowserContext, tabs, cookies, netLog / console / body rings. Each MCP
client and the REST profile holder get a thin BrowserSession facade
(src/browser-session.ts) with their own currentTabId and named-snapshot
store. Two concurrent agents on the same profile no longer clobber each
other's "current tab"; store_as / diff_against are isolated per
session. A stale active tab — one closed by another session or reaped by
the TTL sweeper — self-heals to a clean "No active tab. Call browser_open
first." rather than "Tab ... not found".
Configurable resources (Tier 1–3)
Knobs that used to be hard-coded, all env-only:
| Env | Default | What |
|---|---|---|
BROWSER_MCP_ACTION_TIMEOUT_MS |
10000 |
Per-action Playwright timeout (click/type/press/hover/select/check/drag). |
BROWSER_MCP_NAV_TIMEOUT_MS |
30000 |
page.goto / navigation timeout. |
BROWSER_MCP_NET_RING |
500 |
Network log ring capacity per profile. |
BROWSER_MCP_CONSOLE_RING |
500 |
Console log ring capacity per profile. |
BROWSER_MCP_BODY_RING |
50 |
Captured-response-body ring capacity per profile. |
BROWSER_MCP_BODY_MAX_BYTES |
262144 |
Max bytes per captured response body. |
BROWSER_MCP_MAX_REQUEST_BYTES |
1048576 |
Max accepted HTTP request body (raising this weakens the guard). |
The network_log / console_log limit zod caps are coupled to their
ring sizes so a client can't ask for more than the ring can hold.
Security
- Log redaction extended to the new tools:
browser_fill_form.fields[].value,browser_configure.extra_headers
(values),browser_storage.value,browser_handle_dialog.prompt_text. - Passive response-body capture opt-out.
browser_network_body
captures small texty/JSON bodies passively (≤256 KiB by default, ring of
50). SetBROWSER_MCP_BODY_RING=0to disable capture entirely. - Storage output capped at
BROWSER_MCP_MAX_CHARSso a huge
localStorage doesn't blow up an MCP response. /apicovered by the shared guard gate — same auth (Bearer +
timing-safe), Origin allowlist, Content-Type, body-size, and
max_sessionscap as/mcp.insecureStartupProblem()now mentions
/apitoo.- Transitive vuln patches via lockfile-only
npm audit fix
(fast-uri 3.1.0 → 3.1.2 high; hono / qs / ip-address /
express-rate-limit moderate). No package.json bumps.
HTTP + session lifecycle
Sessiondiscriminated union ({ kind: "mcp", server, transport, … } | { kind: "rest", view, handlers, … });cleanupSessionand
shutdownAppbranch onkindso the REST holder's lack of
transport/server is honest in the lifecycle. Both kinds share
max_sessionsand the 60 s reaper interval; the browser is shut down
only when no entry of either kind still references the profile.- Single tool registry.
src/registry.tsexportsTOOLS: ToolDef[]as
the source of truth for both MCPbuildServerand RESTrunTool.
Adding a tool is one edit in one place. - Bounded shutdown.
BrowserContext.close()inshutdown()is now
raced with an 8 s timeout so a wedged Chromium can't hang the
supervisor's SIGTERM path or test teardown.
Quality + fixes
browser_cookies setvalidates each entry has eitherurlor both
domainandpath(zod.refineat the array element); returns
isError(400 on REST) instead of letting Playwright complain
cryptically.browser_scrollreports "page fits viewport" instead of "100%" when
the content is shorter than the viewport — distinguishes "scrolled to
bottom of a long page" from "nothing to scroll".- Validation failures return
isError, not thrown exceptions, for
type/download_wait/cookies set/upload— uniform error
shape across tools, matches what the MCP SDK expects. - Sandbox path descriptions corrected in tool docs (
browser_save,
browser_download_wait,browser_upload) — the example paths now
match the actual sandbox roots. - Dropped dead
cookies.tab_idparameter; viewport dims required as
a pair (width and height) — schemas tightened.
Tests
- 470 tests across 48 files (was 361 / 31). Phase additions: a tool
registry test (names + shape), purerunToolstatus-mapping unit, an
OpenAPI generator unit (all 36 schemas convert), full REST surface
integration (happy path / 400 / 404 / 405 / lifecycle / cross-surface
ref-count / OpenAPI endpoint), a dedicated REST guards file (401 / 415
/ 403 / 503), per-tool structured-dataassertions in the mock-handler
suite, and BrowserSession delegation tests. - Coverage (
vitest.config.tsthresholds 90 / 85 / 80 / 90): current
92.1 / 85.8 / 89.1 / 93.9. vitest.config.tsmigrated to top-levelmaxWorkers: 4(Vitest 4
deprecatedpoolOptions.forks.maxForks); CI gained step-level timeouts
so a wedgednpx playwright installcan't run for hours.
Repo layout
- Converted to an npm workspace. The server now lives under
apps/browser-mcp/; the JS client and any future shared packages under
packages/(currentlypackages/browser-client-js/). Root-level
scripts delegate into the app with-w, sonpm run build/
npm teststill work from the repo root.
Docs
- New REST API (for scripts) section in the README: model, endpoints
table, response envelope, status codes, curl + JS examples, limitations. - Architecture section documents the two-surface model: the single tool
registry, the sharedBrowserManager+ ref-count across MCP and REST,
the per-profile REST holder lifecycle, and the shared guard gate. - Companion client README with a tool catalogue grouped by purpose, a
typed-surface example showing tsc catching misspellings, and
instructions to regenerate types when the server adds a tool. - Tools reference gained a Locator conventions subsection explicitly
documenting the per-tool divergences (form tools default tolabel;
uploaduses a narrower set;draghassource_/target_prefixes;
downloadlacksexact). - README counts kept in sync (tools, tests, files).
Breaking changes
target_typedefault unified totext.browser_type,
browser_expect, andbrowser_download_waitpreviously required (or
defaulted to) a different locator type. They now default totextlike
browser_click— pass an explicittarget_type: "selector"(orrole,
label, …) if you were relying on the old behaviour.- Legacy
selectoralias removed frombrowser_type. The deprecated
selectorparameter — a stand-in fortargetwhentarget_typewas
unset — is gone.targetis now required.
The MCP wire surface itself stays additive otherwise — every 0.2.0 client
keeps working without changes outside the locator-default tweak above.
Companion package
@graphmemory/browser-client0.1.0 — new workspace package at
packages/browser-client-js/. Initial release; separately versioned.