Browser Session MCP
Browser Session MCP is an MCP server that gives each AI agent an isolated, persistent real Chrome session with browser automation, inspection, logging, stealth, emulation and human-login workflows.
Create, reuse, list and close named or unnamed browser sessions, each with separate cookies, storage and tabs.
Manage tabs and navigate pages, including waiting for load, network idle or page text.
Inspect pages with screenshots, accessibility-tree snapshots, Jev-powered yes/no/option questions and content reading.
Interact using real CDP input: click, tap, swipe, type, select options, upload files, press keys, scroll, move the mouse, wait and run JavaScript.
Drive multi-step goals with
act, using plain-language Jev element selection and optional confirmation for irreversible clicks.Collect per-visit logs: console messages and network requests, with optional headers and small response bodies.
Measure page performance, start/stop V8 CPU profiling, and analyze long tasks and hot functions.
Toggle stealth to reduce CDP automation detection, with tradeoffs for console capture and
evaluate.Throttle CPU/network and add/remove init scripts for slow-device or pre-load instrumentation testing.
Request human takeover links so a person can log in with passwords, passkeys or 2FA while credentials stay out of the agent, then await hand-back.
Save, load, list and delete cookie-based browser states to reuse logins; states are plaintext and cookie-only.
Requires an existing Chrome with its DevTools port open; optional roles add logs, idle reaping, takeover and emulation.
Allows AI agents to navigate complex web interfaces and bypass bot checks, including performing human-assisted logins on high-security sites like GitHub, by presenting live views of login forms to the user.
browser-session-mcp
Give every AI agent its own real Chrome, one that keeps its logins, records what the page did, and gets past bot checks. browser-session-mcp is an MCP server that hands each agent an isolated, long-lived browser session. When a site needs a password or passkey, the agent sends you a link, you log in on a live view of the page, and the agent carries on without ever seeing your credentials.
A browser per agent. Each session has its own cookies, storage and tabs. Run many agents side by side on one Chrome without them seeing each other.
Sessions that stick around. A session is just an id the agent passes to each tool, so it survives dropped connections and server restarts.
Hand logins to a human. For passwords, passkeys and 2FA, the agent sends you a link to a live view of the page. You log in, hand control back, and the agent continues while you watch. Your credentials never reach it.
Log in once. Save a session's cookies under a name and load them into any future session.
Everything recorded. Every console message and network request is kept on disk, grouped by page visit, for the agent to read back while debugging.
Gets past bot checks. No
Runtime.enabletell, a consistent User-Agent and Client Hints, no injected scripts unless you add one, and real mouse and keyboard input. Stealth can be switched on and off per session.Test like a slow phone. Throttle a session's CPU and network, and run your own script before the page's to time what happens from the first byte.
42 tools for tabs, navigation, clicks and typing, screenshots, accessibility snapshots, logs, page speed, CPU profiles and throttling.
Quick start
You need a Chrome with its DevTools port open (this server drives one, it doesn't bundle one) and an MCP client such as Claude Code.
1. Start Chrome. Any Chrome or Chromium works. No Chrome around? Get a
small headless one with npx @puppeteer/browsers install chrome-headless-shell.
chrome-headless-shell --remote-debugging-port=9222 &
curl -s http://localhost:9222/json/version # prints JSON when it's ready2. Add the server to your MCP client. STATE_DIR is where it keeps
sessions, logs and saved logins. With Nix, there's nothing to install:
claude mcp add browser-session -e BROWSER_URL=http://localhost:9222 \
-e STATE_DIR="$HOME/.local/state/browser-session-mcp" \
-- nix run github:xav-ie/browser-session-mcp -- mcpbrew install xav-ie/tap/browser-session-mcp
claude mcp add browser-session -e BROWSER_URL=http://localhost:9222 \
-e STATE_DIR="$HOME/.local/state/browser-session-mcp" \
-- browser-session mcpEach release has a
tarball per platform with the browser-session binary, the takeover page and
systemd units. The Linux binaries are static and run on any distribution; the
macOS one runs on Apple silicon with macOS 14 or newer.
ver=$(curl -fsSL https://api.github.com/repos/xav-ie/browser-session-mcp/releases/latest | sed -n 's/.*"tag_name": "\(.*\)".*/\1/p')
case "$(uname -sm)" in
"Linux x86_64") target=x86_64-unknown-linux-musl ;;
"Linux aarch64") target=aarch64-unknown-linux-musl ;;
"Darwin arm64") target=aarch64-apple-darwin ;;
esac
base=https://github.com/xav-ie/browser-session-mcp/releases/download/$ver
curl -fsSLO "$base/browser-session-$ver-$target.tar.gz"
# check it (on macOS, use shasum -a 256 -c instead of sha256sum -c)
curl -fsSL "$base/SHA256SUMS" | grep "browser-session-$ver-$target.tar.gz" | sha256sum -c -
tar -xzf "browser-session-$ver-$target.tar.gz"
claude mcp add browser-session -e BROWSER_URL=http://localhost:9222 \
-e STATE_DIR="$HOME/.local/state/browser-session-mcp" \
-- "$PWD/browser-session-$ver-$target/browser-session" mcp3. Ask your agent to use it.
Open a browser session, go to example.com, and take a screenshot.
The agent calls open_browser_session, gets back a sessionId, and passes it
to every tool after that:
const { sessionId } = (await tools.browser_session_mcp.open_browser_session({})).structuredContent;
await tools.browser_session_mcp.navigate({ sessionId, url: "https://example.com" });
await tools.browser_session_mcp.take_screenshot({ sessionId });That's all you need to start. The mcp role works on its own. Run the
listener too if you want console and network logs, the reaper to clean up
idle sessions, and takeover for human logins; Deployment
shows how.
On Claude Code, install thebrowser-session plugin for a browser-session-navigator
agent that already knows how to drive these tools.
Related MCP server: Umbra
Handing a login to a person
When a page needs credentials the agent must not see, it asks for a takeover link and shows it to you. The page opens a live view of the agent's tab with you in control, and the agent's calls on that session wait. You log in there, click Hand back, and the agent picks up where it left off. The link stays open so you can watch it work; Take control pauses it again, and End closes the link. Closing the page ends the link after a minute.

The link works for one browser only: the first to open it claims it, and it expires after 30 minutes by default. Workflows has the details and the code.
Picking elements with Jev
Set TYPESAFE_API_KEY and agents stop writing CSS selectors. Describe the
element in plain words and Jev, a small decision model,
picks it in a few hundred milliseconds for a fraction of a cent, instead of a
large model reading the whole page to name a selector.
click({ sessionId, selector: "the login button" })
type({ sessionId, selector: "the email field", text: "me@example.com" })
act({ sessionId, goal: "add the cheapest oolong to the cart" })
ask({ sessionId, question: "Is the oolong in the cart?" })
read({ sessionId, question: "What is the return policy?" })A selector is tried as CSS first, so nothing changes for agents that already
write CSS. act runs a whole multi step goal on the server, one Jev call per
step, and reports whether it finished, got stuck or hit a login wall. ask
answers a yes/no or multiple choice question about the page, and read
returns only the parts of a long page that answer a question. Text to
type stays out of the model, and every miss is an error, never a guess.
Picking elements with Jev covers the benefits, what Jev sees,
and tuning.
All 42 tools
Most tools take a sessionId first. Tools marked ⚙ need one of the optional
background roles from Deployment.
Tools that return JSON declare its shape as an MCP outputSchema, so clients
can type their results. One call drives a session at a time: a second call on
the same session, from any client, waits up to 15 s for the first to finish.
Every tab answers the page's own dialogs (alert, confirm, prompt,
"Leave site?") by accepting them, reported in the next action's result, and
turns passkey prompts down, so sites fall back to a password or a code.
Group | Tools |
Sessions |
|
Tabs |
|
Navigation |
|
Seeing the page |
|
Acting on it |
|
Logs ⚙ |
|
Page speed and profiling |
|
Stealth |
|
Slow devices ⚙ |
|
Human takeover ⚙ |
|
Saved logins |
|
Sessions
open_browser_session({ device?, viewport?, useMobileUA?, userAgent?, name? })→{ sessionId, pageCount, activeUrl }:deviceis a preset (pixel-7,galaxy-s23,iphone-15,iphone-se,ipad-air,laptop,desktop-1080p) setting the screen, pixel ratio, User-Agent and touch;useMobileUAmakes it a touch phone (mobile UA, device metrics and a touch screen);viewport.deviceScaleFactorsets the pixel ratio anduserAgentreplaces the User-Agent, overriding a preset's;namereuses the open session with that name, or opens one with the login saved under it, and saves its login when a takeover finishes and when it closesclose_browser_session({ sessionId }): safe to call twice; a named session saves its login firstlist_browser_sessions()
Tabs
new_page({ sessionId, url? }): opens a tab; later calls act on itlist_pages({ sessionId }): in a stable order, the active tab marked*switch_tab({ sessionId, tabIndex }): makes a tab active and brings it to the frontclose_tab({ sessionId, tabIndex })
Navigation
navigate({ sessionId, url, timeout?, waitUntil?, text?, settleTimeout? }): returns after the load event, or withwaitUntil: "networkidle"once requests go quiet, or oncetextshows, for pages that render themselves after loading
Seeing the page
take_screenshot({ sessionId, fullPage?, selector?, scale?, save? }): a PNG of the screen, the whole page or one element;savewrites it under$STATE_DIR/screenshots/<sessionId>/, kept after the session closes, and returns{ path, bytes }instead, for callers that can't take an imagetake_snapshot({ sessionId }): the accessibility tree as indented text; an unnamed control shows what its icon is, asbutton (icon "trash")ask({ sessionId, question, options? })→{ answer, probability, probabilities? }: asks Jev about the page as it is now, yes/no or which of youroptionsfits, in one cheap callread({ sessionId, question, maxChars? })→{ text, pageChars, passages, kept, filtered }: only the passages of the page that help answerquestion, under their headings; a page under 8,000 characters comes back whole
Acting on it (real CDP input, not synthetic JavaScript events)
act({ sessionId, goal, values?, maxSteps? })→{ status, goal, steps, url, title }: a Jev-driven loop toward a goal — click, type (fromvalues), select, scroll, wait or back per step, checking goal-met, dead-end, login and error walls each round until it finishes, gets stuck or runs out of budgetclick({ sessionId, selector | x,y, timeout? })→{ url, title, navigated, focused, dialog?, hit }:selectormay be plain language or ajev:<goal>— the Jev decision model picks the element (see Picking elements with Jev). Fails, naming the element in the way, when a popup or overlay covers the target or the point is off the screen, and otherwise says where the page is after: its URL, whether it navigated, the focused element and an open dialog.tap,swipeandpress_keyreport the sametap({ sessionId, selector | x,y, timeout? }): a finger tap (touch events), for mobile sessionsswipe({ sessionId, selector? | x,y?, direction? | toX,toY, distance?, durationMs? }): a finger swipe, e.g. to close a drawer or move a carousel; short and slow drags partwaytype({ sessionId, selector, text, delay?, clear?, timeout? }):selectormay be plain language or ajev:goal; the text itself always comes from you, typed after what the field holds unlessclearselect_option({ sessionId, selector, option?, timeout? })→{ chosen, options, shown, confirmed, ... }: picks an option by its text in a native<select>, a radio group or a dropdown drawn by scripts, with or without ARIA roles, in frames too (it opens it, types to filter only lists that may be partial, clicks the option and reads the field back); withoutoptionit lists the options and closes the list, choosing nothing; fails listing the options when none matchesupload_file({ sessionId, selector, content?, name?, path?, mimeType?, timeout? })→{ name, bytes, how, into, pageShowsName, ... }: attaches a file given as base64content(or apathon the server's machine) to a file input, or to the hidden one behind an Attach button or label; an element with no input behind it gets the file dropped on itpress_key({ sessionId, key, selector?, repeat?, timeout? }):keyis a name (Enter, Tab, ArrowDown…) or a character;selectormay be plain language or ajev:goal and is optional;repeatpresses it up to 50 times, withfocusTraillisting focus after each press;click,tapandpress_keylist a dropdown'soptionswhen the action leaves one openscroll({ sessionId, deltaY?, deltaX?, x?, y? })move_mouse({ sessionId, x, y })wait_for({ sessionId, selector? | text? | expression? | ms?, timeout? }): a CSS selector, page text or a JavaScript expression turning truthy (nojev:here — it's a poll, not an action);msalone just pauses, for hosts with no timerevaluate({ sessionId, expression, tabIndex? }): runs JavaScript and returns the value
Logs ⚙ (written by the listener role)
list_visits({ sessionId }): one entry per page visit, oldest firstlist_console_messages({ sessionId, visit?, limit? }):limitdefaults to 500list_network_requests({ sessionId, visit?, limit?, urlContains?, details? }):limitdefaults to 500;detailsadds request and response headers and small text or JSON response bodies (fetch/XHR and documents, up to 64 KB), credential headers and keys masked and nothing kept from a takeover login. Includes requests from cross-site iframes and workers (sandboxed third-party code such as a checkout page's tracking pixels), logged under the tab's visit
Page speed and profiling
measure_page({ sessionId, url?, timeout? }): reloads (or loadsurl) and returns server response, first and largest paint, layout shift, blocking time, and bytes by type and host, rated by the Core Web Vitals thresholds, in the session's device and throttling
Find the JavaScript behind a slow or hung page:
start_profiling({ sessionId, tabIndex?, samplingIntervalUs? }): starts the V8 CPU profilerstop_profiling({ sessionId, tabIndex?, top?, terminateAfterMs?, timeout? }): saves a.cpuprofile(open it in DevTools or speedscope.app) under$STATE_DIR/logs/<sessionId>/profiles/and returns long tasks, the hottest functions and time per script. If the page is hung, its script is stopped afterterminateAfterMs(default 5000) so the profile can be collected
Stealth
set_stealth({ sessionId, enabled })get_stealth({ sessionId })→{ stealth }
Slow devices ⚙ (applied by the listener role to every tab of the session, including ones opened later)
set_emulation({ sessionId, cpuThrottlingRate?, network? })→{ cpuThrottlingRate, network, initScripts }: CPU slowdown (1 to 100) and network throttling, a DevTools preset (slow3g,slow4g,fast4g,offline,off) or{ latencyMs, downloadKbps, uploadKbps }; in place on every open tab when it returnsget_emulation({ sessionId })→{ cpuThrottlingRate, network, initScripts }add_init_script({ sessionId, source })→{ id, ... }: runssourcein the main world at the start of every page load, before the page's own scripts, from each tab's next navigationremove_init_script({ sessionId, id })
Human takeover ⚙ (needs the takeover role and TAKEOVER_BASE_URL)
request_human_takeover({ sessionId, ttlMinutes? })→{ url, token, expiresAtMs }: returns right away with a linkawait_human_takeover({ token, timeout? })→{ completed, watching, expired, url?, title? }: waits up totimeout(default 25 s) for the person to hand control back; call it in a loop untilcompleted, or untilexpiredsays to request a new link.watchingmeans the link is still open: if they take control again, calls on the session fail until you await the same token again
Saved logins
save_browser_state({ sessionId, name })load_browser_state({ sessionId, name })list_browser_states()delete_browser_state({ name })
Learn more
Workflows: handing a login to a person, and saving cookies to reuse.
Picking elements with Jev: plain-language selectors and the
actgoal loop, what they cost, and what the model sees.How it works: why a session outlives its connection, the four roles, how stealth works, and what's on disk.
Deployment: building from source, the NixOS module, systemd without Nix, and every environment variable.
Development: checks, diagrams and releases.
License
Available Tools
42 toolsactADestructive
Act toward a goal on the session's active tab in one call: the server extracts the page's interactive elements (including open shadow roots and same-origin iframes) and visible text, asks the Jev decision model for the next step, does it (click, type, select, hover, right-click, drag, scroll, back, wait, or press a key such as Enter, Escape, Tab or an arrow; on a mobile session it taps instead of clicking, drags with a finger, and can swipe an element or the screen), and repeats until the goal reads achieved, nothing looks able to make progress, or the step budget runs out. Each step is one cheap Jev request (~150 to 500 ms), not a driving-LLM round trip, so multi-step flows take seconds and fractions of a cent. Pass values (e.g. { query: "Oolong" }) for anything to type: Jev only sees the keys and picks which field each belongs to; the text goes straight to the page, so secrets never reach a model. JavaScript dialogs are accepted and listed in dialogs, and a link that opens a new tab is followed there. Needs TYPESAFE_API_KEY; every step lands on an element the extraction just offered, and when Jev isn't sure of a step it stops with status uncertain and the candidates it was weighing instead of guessing. status is completed, likely_completed (Jev leaned toward done but wasn't sure), uncertain, stuck, needs_login (pair with request_human_takeover or load_browser_state; a run with values keeps going at a login form), needs_human (a CAPTCHA or bot check; use request_human_takeover), needs_confirmation (only with confirmIrreversible; pending names the click it held back), blocked (an error page), no_progress or max_steps. Returns a per-step trace with confidences. Jev can be eager to call a goal met: verify with take_snapshot or take_screenshot afterwards. For a single action, click/type with a plain-language selector is cheaper.
Inputs: goal (required), sessionId (required), values, maxSteps, +1 more. Returns: url, goal, steps, title, +4 more
add_init_scriptA
Run JavaScript at the start of every page load in every tab of the session, before any of the page's own scripts, until removed or the session closes. Use it to record timings or DOM changes from the very first moment of a load (a MutationObserver, PerformanceObserver or console hook writing to a window global you read later with evaluate), or to stub an API before the page uses it. Takes effect from each tab's next navigation, not the page already loaded. It runs in the page's main world, so the page can see what it defines. Returns an id for remove_init_script. Needs the listener role.
Inputs: source (required), sessionId (required). Returns: id, network, initScripts, cpuThrottlingRate
askARead-only
Ask the Jev decision model a question about the session's active tab and get a calibrated answer in one cheap call (~150 to 500 ms, a fraction of a cent), judged on the page's URL, title and visible text (an open dialog's first; up to 20,000 characters). Without options it is a yes/no question, e.g. "Is an error message showing?" or "Is the user signed in?", answered with the probability of yes. With options it picks the one that fits, e.g. question "Which page is this?" with ["product page", "cart", "checkout", "error page"], with each option's probability. Useful for branching a script without a large model, and for checking a result after act. Jev reads and judges; it is weak at counting, arithmetic and dates, so ask those of evaluate or take_snapshot instead. Needs TYPESAFE_API_KEY; any Jev problem is an error, never a default answer. It reads the page without changing it.
Inputs: question (required), sessionId (required), options. Returns: answer, probability, probabilities
await_human_takeoverA
Wait up to timeout (default 25 s) for the person to hand control back on the takeover page, or for the link to end. Returns { completed, watching, expired, url, title }: completed true once they handed back, with the page's URL and title; otherwise false, and call it again in a loop, each call short enough for your host's per-call limit (the default suits Executor). watching true means the link is still open: the person may watch and can take control again, and then calls on the session fail until you await the same token again. expired true means the link ran out unfinished; issue a new request_human_takeover. The person may take several minutes, so poll from a background task if you can. A session opened with a name saves its login under that name as soon as they finish. Fails on a token that request_human_takeover didn't issue or whose link has ended and was already reported.
Inputs: token (required), timeout. Returns: url, title, token, expired, +2 more
clickADestructive
Click in the session's active tab with real CDP mouse input (trusted events — fires handlers reliably and grants user activation, so it can open popups/new tabs that a JS click can't). Pass a CSS selector to click the first match's center, after waiting up to timeout for it and scrolling it into view. Plain language works too: a jev: prefix (e.g. jev: the login button) goes straight to the Jev decision model, and with TYPESAFE_API_KEY configured on the server any selector that matches nothing as CSS is retried as a plain-language goal the same way — the server sends the page's interactive elements to Jev, which picks one (on low confidence or any Jev problem it errors instead of guessing, so nothing is clicked blindly; elements are picked from the page as it is at call time, so wait_for content first if the page is still loading). Or pass both x and y to click viewport coordinates, which are used only when there is no selector. Don't use evaluate to click: synthetic JS clicks can be ignored by pages. Coordinates outside the viewport are not rejected; the click lands on nothing. A click can submit forms or trigger purchases, like a real user's. Returns a confirmation naming what was clicked, and rejected: fields the page refused during the click with their messages (a required field left empty, or the page's own error text), when a form wasn't sent. Fails if the selector matches nothing within timeout and Jev can't (or isn't configured to) resolve it, or if neither a selector nor both coordinates are given.
Inputs: sessionId (required), x, y, timeout, +1 more. Returns: hit, url, title, dialog, +7 more
close_browser_sessionADestructiveIdempotent
Close a browser session for good: closes all its tabs, discards its cookies and storage, and deletes its console and network logs and saved .cpuprofile files. Call it when the task is done so the session doesn't sit open until the reaper closes it, but leave a session open while its login may be needed again soon. Use close_tab instead to close one tab and keep the session. A session opened with a name saves its login under the name first; otherwise save_browser_state first if the login should outlive the session. Idempotent: closing an unknown or already-closed session succeeds, so it is safe in cleanup code. Returns a confirmation.
Inputs: sessionId (required).
close_tabADestructive
Close the tab at tabIndex (from list_pages). The session and its other tabs stay open; if the closed tab was active, the last remaining tab becomes active. Tabs after it shift down one index, so call list_pages again before using another index. Closing the last tab leaves the session open with no tabs; the next navigate opens a new one. To end the whole session, use close_browser_session. Returns a confirmation. Fails if the session has no tab at that index.
Inputs: tabIndex (required), sessionId (required).
delete_browser_stateADestructiveIdempotent
Delete a saved browser state by name, permanently; open sessions that already loaded it keep their cookies. Use it to forget a login that is stale or no longer wanted; to update a state, call save_browser_state with the same name instead. Use list_browser_states to see the names. Deleting a name that doesn't exist succeeds with removed false, so it is safe to repeat.
Inputs: name (required). Returns: name, removed
evaluateADestructive
Run JavaScript in a tab and return its value as JSON in { result }. A bare expression returns its value; for several statements use return ...; promises are awaited. Values that aren't JSON come back reduced: undefined as null and DOM nodes as {}. It runs in the page's main world (can read page globals like window.dataLayer); on stealth sessions (see set_stealth) it runs in an isolated world instead, which sees the DOM but not page-defined globals. tabIndex runs it in another tab without switching to it, and frame in an embedded frame. The code can change the page like any script, so prefer take_snapshot or wait_for when they suffice, and click or type for interaction. Fails with the JS exception's message if the code throws.
Inputs: sessionId (required), expression (required), frame, tabIndex. Returns: result
get_emulationARead-only
Return the session's throttling and init scripts; it changes nothing. A new session has none.
Inputs: sessionId (required). Returns: network, initScripts, cpuThrottlingRate
get_stealthARead-only
Return whether stealth is on for a session; it changes nothing. Check it before evaluate or list_console_messages, whose behavior it changes; use set_stealth to change it. A new session starts with stealth off.
Inputs: sessionId (required). Returns: stealth
list_browser_sessionsARead-only
List every open browser session on the shared Chrome, sorted by sessionId, including ones opened by other agents; it takes no arguments. Use it to recover a lost sessionId or to find sessions left open; don't use it to see the tabs inside one session (list_pages) or saved logins (list_browser_states). activeUrl is null for a session with no tabs. The list is empty if none are open.
Inputs: no named parameters. Returns: sessions
list_browser_statesARead-only
List the saved browser states (cookie sets saved by save_browser_state) that load_browser_state and delete_browser_state accept, sorted by name; it takes no arguments and changes nothing. Use it to pick a name to load; these are saved logins on disk, not open sessions, so use list_browser_sessions for those. The list is empty if nothing has been saved.
Inputs: no named parameters. Returns: states
list_console_messagesARead-only
List console messages (console.log, warnings, errors, uncaught exceptions) the session's pages emitted, oldest first, from the most recent limit. Use it to debug page errors; use list_network_requests for failed requests. Pass visit (a seq from list_visits) to see one page load; without it, messages from every visit are merged and limit keeps the newest. It reads logs and changes nothing. Empty unless the listener role is running, nothing is captured while stealth is on, and logs are deleted when the session closes.
Inputs: sessionId (required), limit, visit. Returns: messages
list_network_requestsARead-only
List the network requests the session's pages made, oldest first, from the most recent limit: method, URL, status (or failure reason), redirects and request body. With details, also request and response headers and, for fetch/XHR and documents, text or JSON response bodies up to 64 KB; credential headers and keys are masked, and nothing of a login a person does in a takeover is kept. urlContains narrows the list to matching URLs. Use it to check API calls, what a page sent and got back, tracking pixels and failed loads; use list_console_messages for script errors and logs. Pass visit (a seq from list_visits) to see one page load; without it, requests from every visit are merged and limit keeps the newest. A null status means the request hadn't finished when logged. It reads logs and changes nothing. Empty unless the listener role is running, and logs are deleted when the session closes.
Inputs: sessionId (required), limit, visit, details, +1 more. Returns: requests
list_pagesARead-only
List the session's tabs in a stable order; it changes nothing. The active tab (the one navigate/evaluate/screenshot/click target by default) is marked with * in the text and active: true in the structured output. Call it before switch_tab, close_tab, or evaluate/profiling with tabIndex, to get a current index; use list_browser_sessions to see sessions rather than tabs, and list_visits for navigation history. Indices stay stable until a tab is closed, after which later tabs shift down one. The list is empty if every tab was closed; the next navigate opens a new one.
Inputs: sessionId (required). Returns: pages
list_visitsARead-only
List the session's page visits, oldest first. Each visit is one top-level navigation in one tab, numbered by seq; pass that number as visit to list_console_messages or list_network_requests to see only that page load's logs. Use it to find which page load to inspect; use list_pages for the tabs open now. It reads logs and changes nothing. Empty unless the listener role is running, and deleted when the session closes.
Inputs: sessionId (required). Returns: visits
load_browser_stateADestructiveIdempotent
Load cookies saved by save_browser_state into this session, usually right after open_browser_session and before navigate, so the session starts logged in. Pages already open need a reload or navigate to use them. Loaded cookies replace any with the same name, domain and path; others are kept. Expired logins won't work again; if the site still asks to log in, use request_human_takeover and save again. Fails if no state has that name; list_browser_states shows the names.
Inputs: name (required), sessionId (required). Returns: name, cookieCount
measure_pageA
Measure how fast a page loads, like a small Lighthouse performance run, in the session as it is: its login, device and throttling (set_emulation). Reloads the active tab (or loads url), waits for its requests to go quiet, and returns server response time, first and largest contentful paint (and the element it was), cumulative layout shift, total blocking time from long tasks, DOMContentLoaded and load times, requests and bytes by type and host, and the largest resources, each metric rated good, needs improvement or poor by the Core Web Vitals and Lighthouse thresholds. Use start_profiling instead to find which JavaScript is slow. Reloading loses unsaved page state.
Inputs: sessionId (required), url, timeout. Returns: cls, url, bytes, fcpMs, +14 more
move_mouseA
Move the mouse to viewport coordinates x,y (CSS pixels from the top-left) in the session's active tab via CDP input (trusted move event), so the page's hover, mouseover and mousemove handlers fire. Use it to open hover menus and tooltips before reading or clicking them, and to add human-like behavioral telemetry. Don't use it to click, drag or scroll: use click and scroll. Coordinates outside the viewport are not rejected; the move just hovers nothing. Returns a confirmation with the position.
Inputs: x (required), y (required), sessionId (required).
new_pageA
Open a new tab in the session and make it the active tab, so later navigate/click/type/screenshot calls act on it. Use navigate instead to load a URL in the current tab, and switch_tab to go back to an existing one. Tabs stay open until close_tab or close_browser_session, so close ones you no longer need. With url, the tab loads it and the call returns after the load event; if the URL fails to load, the call errors but the new tab still exists and is active. The returned index is the new tab's list_pages index.
Inputs: sessionId (required), url. Returns: url, index
open_browser_sessionA
Open a new isolated browser session and return its sessionId; call this first, then pass the id to every other tool. Each session is its own Chrome BrowserContext with separate cookies, storage and tabs, so sessions never see each other, and it starts with one blank tab. Don't open a new session per page: reuse one and use new_page for more tabs. To start logged in, follow with load_browser_state before navigate. Set viewport for a specific window size and useMobileUA to be a phone: a mobile User-Agent and client hints, mobile device metrics (the page's viewport meta applies) and a touch screen (navigator.maxTouchPoints, touch events, pointer: coarse), so sites pick their touch behavior; use tap and swipe on it, and act taps and swipes on its own. Both are fixed for the session's life. Give a name (e.g. github) to keep a login across tasks: opening that name again reuses the session while it is open, and otherwise opens a new one with the login saved under the name, which is saved when a takeover finishes and when the session closes. Sessions persist until close_browser_session, or until the reaper role closes ones idle for MAX_IDLE_HOURS (default 24). Fails if Chrome is unreachable at BROWSER_URL.
Inputs: name, device, viewport, userAgent, +1 more. Returns: activeUrl, pageCount, sessionId
press_keyADestructive
Press a key via CDP keyboard input in the session's active tab. key is a name (Enter, Tab, Escape, Backspace, ArrowDown, Space, …), a single character, or a combination held down together such as Shift+Tab or Control+a (modifiers: Shift, Control, Alt, Meta). With a selector (CSS; or plain language — a jev: goal such as jev: the password field goes straight to the Jev decision model, and with TYPESAFE_API_KEY set a selector that matches nothing as CSS is retried as plain language), waits up to timeout for it and focuses it first (e.g. type into a field then press Enter); without, presses on whatever is focused, and timeout is unused. Use type for whole strings and click to activate buttons. Pressing Enter can submit a form, like a real user. Returns a confirmation. Fails on an unknown key name, or if the selector matches nothing within timeout and Jev can't (or isn't configured to) resolve it.
Inputs: key (required), sessionId (required), repeat, timeout, +1 more. Returns: hit, url, title, dialog, +7 more
readARead-only
Read only the parts of the session's active tab that help answer a question, instead of the whole page: the page's visible text (an open dialog's, when one is showing) is split into passages of about 600 characters under their headings, the Jev decision model rates each one's relevance in parallel requests, and the relevant ones come back in page order under ## heading lines, up to maxChars. A long article shrinks to the few sections that matter, saving the agent's context; a page under 8,000 characters comes back whole without asking Jev. Use take_snapshot for the page's structure and controls, and read for its content. Needs TYPESAFE_API_KEY; any Jev problem is an error. It reads the page without changing it.
Inputs: question (required), sessionId (required), maxChars. Returns: url, kept, text, title, +3 more
remove_init_scriptA
Stop running an init script added by add_init_script; pages already loaded keep what it did. Fails on an id the session doesn't have; get_emulation lists them.
Inputs: id (required), sessionId (required). Returns: network, initScripts, cpuThrottlingRate
request_human_takeoverA
Get a human takeover link (URL) for the session's active page, so a person can log in themselves (passwords/passkeys the agent must not see).
Use it only when a login needs a person; for a site you've logged into before, try load_browser_state first. Show the returned url to the user and keep the token for await_human_takeover. Does NOT block. The person starts in control, and every call on the session except reading logs fails until they hand control back; after that the link stays open for them to watch you work, and they can take control again, which pauses your calls the same way. After showing the URL, run await_human_takeover in a background task (subagent or job), since the human may take many minutes; with no background task, end your turn and call it once the user says they're done. The URL is a bearer credential that gives live control of the tab until ttlMinutes passes, so share it only with the intended person. Fails if TAKEOVER_BASE_URL isn't configured on the server.
Inputs: sessionId (required), ttlMinutes. Returns: url, token, expiresAtMs
save_browser_stateADestructiveIdempotent
Save the session's cookies under a name so a future session can load them with load_browser_state and resume without logging in again. Call it after a successful login (for example once await_human_takeover completes). Saving under an existing name replaces that state, so use a distinct name per site or account. Only cookies are saved, not localStorage, so sites that keep their login in localStorage won't resume. States are stored on disk as plaintext and include session credentials. Fails on a name with characters other than letters, digits, '.', '_' and '-'.
Inputs: name (required), sessionId (required). Returns: name, savedAt, cookieCount
scrollA
Scroll the session's active tab with a real mouse-wheel event at a viewport position; whatever scrollable element is under (x,y) scrolls, so point inside a scrollable panel to scroll it rather than the page. deltaY and deltaX are in pixels; negative values scroll up or left. Useful to reveal lazy-loaded content and to generate human-like behavioral signals. Don't use it just to reach an element before clicking: click scrolls its target into view itself. Returns immediately without waiting for content to load; follow with wait_for if needed.
Inputs: sessionId (required), x, y, deltaX, +1 more.
select_optionA
Choose an option in a dropdown or a radio group by its text, in the session's active tab. Works on a native <select> (set directly, firing input and change), on a radio group (pass a radio, its fieldset or a role=radiogroup; the option is clicked as a person would and checked to be selected) and on custom dropdowns drawn by scripts (a combobox, a React or other component select, a searchable list): it opens the dropdown with a real click, types option to filter when the option isn't in the list showing (long lists drawn in part, and lists searched as you type), clicks the option, and reads the field back. option matches exactly ignoring case, else the shortest option starting with it, else the shortest containing it. selector is the dropdown: the select, the combobox input or the box you'd click to open it (CSS, or plain words as for click). Without option, opens the dropdown, returns its options and closes it, choosing and typing nothing: look before you pick. It types only when the list may be partial (empty, loading, or 20 options or more), and clears what it typed when nothing matches. Returns what was chosen, the options offered, what the field shows after and whether that is the option (confirmed; a field can show a short form, such as "+1" for a country), with where the page is as for click. Fails listing the options offered when none matches, or when no list opens.
Inputs: selector (required), sessionId (required), option, timeout. Returns: hit, url, shown, title, +10 more
set_emulationAIdempotent
Throttle the session's CPU and/or network to measure a page as a slow phone or connection sees it (load time, hydration, layout shift, flaky taps). Applies to every tab of the session, open now or opened later (popups included), and lasts until changed or the session closes; it is in place when this returns, so navigate right after. Omitted fields keep their current value. Typical mid-range phone: cpuThrottlingRate 4 with network "slow4g" and useMobileUA on open_browser_session. Pass cpuThrottlingRate 1 and network "off" to stop throttling. Needs the listener role; fails if no tab confirms within 5s.
Inputs: sessionId (required), network, cpuThrottlingRate. Returns: network, initScripts, cpuThrottlingRate
set_stealthAIdempotent
Toggle stealth for a session. enabled: true disables the CDP Runtime domain (removes the Runtime.enable automation tell that anti-bot systems like Cloudflare detect) — load/navigate pages in this mode to pass CDP-based bot gates; the cost is no console capture and evaluate runs in an isolated world (can't read page globals like window.dataLayer). enabled: false re-enables Runtime: console capture + main-world evaluate (reads page globals) return, but the CDP tell is present. Typical flow: set_stealth(true) → navigate → set_stealth(false) → evaluate. Leave it off for sites without bot detection.
Inputs: enabled (required), sessionId (required). Returns: stealth
start_profilingA
Start the V8 CPU profiler on a tab (active tab by default, or tabIndex). Use to find which JavaScript makes a page slow or unresponsive: start, reproduce the problem (navigate/click/wait), then call stop_profiling on the same tab. Don't use it for network slowness; list_network_requests shows that. The 1000µs default suits most slowness; lower samplingIntervalUs (e.g. 100) only to catch functions shorter than a millisecond, at the cost of more overhead and a larger profile. Profiling continues until stop_profiling, so always stop it. Returns a confirmation. Fails if that tab is already being profiled; the running recording continues.
Inputs: sessionId (required), tabIndex, samplingIntervalUs.
stop_profilingADestructive
Stop the CPU profiler started by start_profiling, save a .cpuprofile under $STATE_DIR/logs//profiles/ (its path is in the result; open it as a flame graph in the DevTools Performance panel or speedscope.app) and return the culprits: long tasks (>=50ms) split into JS vs browser-native (parse/style/layout) vs GC time with their top scripts, top JS functions and heaviest JS stack, top functions by self and total time (with url:line:col), and self time per script. Pass the same tabIndex given to start_profiling. If page JS is hung, the renderer can't answer until it yields, so after terminateAfterMs the running script is terminated to get the profile out (terminated in the result says so); set it to 0 to never terminate. top caps each ranked list, so raise it when the culprit isn't in the first 15, and keep timeout a few seconds above terminateAfterMs so a large profile has time to transfer. The file is deleted with the session by close_browser_session. Fails if start_profiling wasn't called on that tab.
Inputs: sessionId (required), top, timeout, tabIndex, +1 more. Returns: path, busyMs, samples, topSelf, +5 more
swipeADestructive
Swipe a finger across the session's active tab with real CDP touch input (touchstart, touchmove, touchend, and the matching pointer events), e.g. to close a drawer, move a carousel, pull to refresh or scroll the way a phone does. Start on an element (selector, or plain words as for click), at x,y, or by default the middle of the screen; end distance pixels away in direction (default 60% of the screen along that axis) or exactly at toX,toY. durationMs sets the speed (default 300): short and slow drags a drawer partway, to check it snaps back. Use it on mobile sessions (open_browser_session with useMobileUA); pages that listen only for mouse events won't react. Returns where the swipe started and ended.
Inputs: sessionId (required), x, y, toX, +6 more. Returns: hit, url, title, dialog, +7 more
switch_tabAIdempotent
Make the tab at tabIndex the active tab, so later navigate/evaluate/screenshot/click/type calls target it, and bring it to the front in Chrome (the human-takeover live view follows it too). Use new_page to open a tab instead, and pass tabIndex straight to evaluate or the profiling tools to act on another tab without switching. Indices come from list_pages and shift when a tab closes, so list again if unsure. Fails if the session has no tab at that index.
Inputs: tabIndex (required), sessionId (required). Returns: url, index
take_screenshotARead-only
Capture a PNG of the session's active tab, or one element of it, and return it as an image or, with save, as a file path. Use it when visual layout, styling or images matter; use take_snapshot for a cheaper text view of the page's structure, and evaluate to read specific values. fullPage captures the whole scrollable page instead of the visible viewport, which can make a very large image on long pages. It reads the page without changing it. Returns the image plus a caption with its size in bytes.
Inputs: sessionId (required), save, scale, fullPage, +1 more.
take_snapshotARead-only
Return the accessibility tree of the session's active tab as indented text: each node's role and name (links, buttons, headings, form fields and their labels, and current values), nested by structure. A control the page gives no name shows what its icon is, as button (icon "trash"), read from the icon's markup. Use it to find what's on the page and what to click or type into, far more cheaply than take_screenshot; use take_screenshot when visual layout matters, and evaluate for data not shown as text. It reads the page without changing it and returns the whole tree, so a large page gives long output.
Inputs: sessionId (required).
tapADestructive
Tap in the session's active tab with a finger: real CDP touch input, so the page gets touch events, pointer events with pointerType "touch" and the click a tap makes. Use it on mobile sessions (open_browser_session with useMobileUA, which also makes the page see a touch screen) for anything a phone user would tap; click sends mouse input instead. Pass a CSS selector (or plain words, resolved by Jev as for click) to tap the first match's center after waiting up to timeout for it, or both x and y for viewport coordinates. A tap can submit forms or trigger purchases, like a real user's. Returns a confirmation naming what was tapped.
Inputs: sessionId (required), x, y, timeout, +1 more. Returns: hit, url, title, dialog, +7 more
typeADestructive
Type text into the first element matching a CSS selector in the session's active tab: waits up to timeout for it, clicks it to focus, then sends real key events for each character, so the page's input handlers fire. Plain language works too: a jev: prefix (e.g. jev: the email field) goes straight to the Jev decision model, and with TYPESAFE_API_KEY set on the server a selector that matches nothing as CSS is retried as a plain-language goal — Jev picks the input from the page's interactive elements (it errors rather than guesses); the text itself always comes from you. Use press_key afterwards for Enter or Tab, and press_key alone for single keys. clear empties the field first; without it, text is appended. delay paces the characters for pages that react to typing speed. A date, time, datetime-local, month, week, range or color input takes no typed text, so its value is set directly with the events the page listens for: dates as 2026-03-14 or 03/14/2026, times as 14:30 or 2:30 PM, a range as a number. Afterwards it reads the field back: it reports text the page reformatted (a phone mask), and fails when the field is empty afterwards (the page dropped the text). Target an input, textarea or contenteditable element; typing into a button reports success but changes nothing. Returns how many characters were typed, or the value set. Fails if the selector matches nothing within timeout and Jev can't (or isn't configured to) resolve it.
Inputs: text (required), selector (required), sessionId (required), clear, +2 more.
upload_fileA
Attach a file in the session's active tab, as if picked in the file dialog (which can't be driven): the page gets a real File with input and change events. selector is the file input, or what stands for it on the page: an "Attach" or "Upload" button, its label or the box around it, whose hidden file input is used. An element with no file input behind it is treated as a drop zone and receives the drag and drop a person dropping the file would make. Give the file as a path the server can read, or as content (base64) with its name. Prefer path: content passes the whole file through your context. path is on the machine the server runs on, which may be a container that doesn't see your files; send content then. Up to 25 MB. Returns what received the file and whether the page then shows its name, with where the page is as for click; warns when the input's accept list doesn't take the file.
Inputs: selector (required), sessionId (required), name, path, +3 more. Returns: hit, how, url, into, +13 more
wait_forARead-only
Wait until a CSS selector matches an element, a text string appears in the page's visible text, or a JavaScript expression is truthy, in the session's active tab, polling every 100ms; or, with only ms, just pause. Use it after navigate, click or scroll when content loads asynchronously. Pass one of selector, text or expression; selector wins if several are given. ms alone is a plain pause for hosts with no timer of their own. text is a case-sensitive substring of document.body.innerText, so text hidden with CSS doesn't count; it is looked for in frames from other sites too. It reads the page without changing it. Returns which selector or text matched. Fails with a timeout error if nothing matches within timeout, or if neither argument is given.
Inputs: sessionId (required), ms, text, frame, +3 more.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 42 tools
Most tools have clearly distinct purposes, and the descriptions explicitly cross-reference alternatives such as click vs tap vs act, and take_snapshot vs read vs evaluate. Some overlap remains among interaction primitives and page-reading tools, but the boundaries are well documented.
All names use snake_case and are mostly readable verb-based names. However, many core tools are bare verbs (click, type, scroll, navigate, evaluate) while others follow verb_noun, so the pattern is not fully uniform.
With 42 tools, the surface is heavy for a browser automation server. While many features are legitimate, advanced capabilities like profiling, emulation, stealth, init scripts, and takeover add significant selection overhead.
The tool set covers session lifecycle, tabs, navigation, interaction, screenshots/snapshots, JavaScript evaluation, console/network logs, performance profiling, stealth, emulation, saved state, and human takeover. Only minor gaps such as explicit download handling or cookie inspection are apparent.
Maintenance
Related MCP Connectors
Give AI agents a real cloud Chrome: open sessions, navigate, read pages, click, type and screenshot.
Cloud browsers for AI agents: saved logins, residential IPs, live human takeover, 57 browser tools.
Cloud browsers for AI agents: get past bot detection, sign in with MFA, run web tasks at scale.
Undetectable cloud browser sessions for AI agents and scrapers. Navigate, extract, click, captcha.
Related MCP Servers
- AlicenseBqualityDmaintenanceStealth Chrome MCP server for AI agents providing 77 tools, perfect bot detection scores, proxy pools, encrypted sessions, and live human handoff via Cloudflare tunnel.87MIT
- AlicenseAqualityCmaintenanceUmbra is the trust layer for AI-generated code. It scores any repo 0-100 with file:line evidence (security, slop, Docker-verified build/boot, and claim receipts that catch agents lying about tests), and guards agent file writes mid-stream blocking leaked keys, alg:none JWTs, and git-hook plants before they land. Tools: scan_repo, guard_content, get_score.384 npm1MIT
- FlicenseNot gradedqualityAmaintenanceExposes a local MCP endpoint that lets AI agents and human operators drive isolated Chromium profiles — each with its own cookies, fingerprint, and proxy — through tools for navigation, clicking, typing, extraction, screenshots, JS evaluation, tabs, and cookie management. Persistent per-profile session state keeps logins and context alive across launches and across AI sessions, so agents can complete real authenticated workflows.-
- AlicenseNot gradedqualityAmaintenanceA local MCP server that lets AI assistants drive your real, already-logged-in Chrome through a browser extension, so agents can read, click, type, navigate, screenshot, inspect network traffic, and manage cookies or storage on pages that require existing sessions. It works without opening a remote-debugging port and keeps all communication on your machine.1 npmISC