# browsertap-mcp English | [中文文档](https://github.com/LinVireo/browsertap-mcp/blob/main/README.zh-CN.md) [![Offline CI](https://github.com/LinVireo/browsertap-mcp/actions/workflows/test.yml/badge.svg)](https://github.com/LinVireo/browsertap-mcp/actions/workflows/test.yml) [![Python 3.10+](https://img.shields.io/badge/Python-3.10%2B-3776AB.svg)](https://github.com/LinVireo/browsertap-mcp/blob/main/pyproject.toml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/LinVireo/browsertap-mcp/blob/main/LICENSE) [Usage guide](https://github.com/LinVireo/browsertap-mcp/blob/main/docs/USAGE.md) · [Troubleshooting](https://github.com/LinVireo/browsertap-mcp/blob/main/docs/TROUBLESHOOTING.md) · [Security](https://github.com/LinVireo/browsertap-mcp/blob/main/.github/SECURITY.md) · [Privacy](https://github.com/LinVireo/browsertap-mcp/blob/main/PRIVACY.md) · [Contributing](https://github.com/LinVireo/browsertap-mcp/blob/main/.github/CONTRIBUTING.md) · [Changelog](https://github.com/LinVireo/browsertap-mcp/blob/main/CHANGELOG.md) **Browser automation for the Chrome, Edge, or Opera you already use.** BTAP connects your MCP client to a browser extension, reusing your open tabs and logged-in profile. Your agent can read pages, fill forms, download attachments, and inspect network activity. Page input runs in the selected tab in the background by default, without moving your desktop cursor. BTAP controls a real browser profile, not a disposable sandbox. Give it access only to accounts and data that the connected agent may use. First-time extension installation is manual; [setup](#start-in-60-seconds) and [security](#disclaimers) are below. ## Start in 60 seconds **Claude Code and Codex: install the plugin.** It includes the MCP server and caller Skills. Install [uv](https://docs.astral.sh/uv/getting-started/installation/) first; dependency downloads and the manual browser step can take longer than a minute. 1. **Add BrowserTap to your agent.** Choose your client: Claude Code: ```bash claude plugin marketplace add LinVireo/browsertap-mcp claude plugin install browsertap-mcp@browsertap ``` Codex: ```bash codex plugin marketplace add LinVireo/browsertap-mcp codex plugin add browsertap-mcp@browsertap ``` 2. **Load the browser extension manually.** Open a new agent session and ask it to call `get_setup_status` for the `extension_path`. Open `chrome://extensions`, enable **Developer mode**, choose **Load unpacked**, and select that directory. Use `edge://extensions` or `opera://extensions` for those browsers. 3. **Start a browser task:** "List my open tabs, then summarize the page I select without navigating or closing it." [Plugin setup and updates](https://github.com/LinVireo/browsertap-mcp/blob/main/docs/PLUGINS.md) also covers `browsertap doctor` through the plugin's environment. For Cursor, Claude Desktop and other MCP clients, use the [standard MCP installation](#getting-started) below. Existing MCP configurations continue to work. ## Documentation by audience | Reader | Start here | | --- | --- | | Installing or using BTAP | This README, then the [usage guide](https://github.com/LinVireo/browsertap-mcp/blob/main/docs/USAGE.md) for workflows and boundaries. | | Installing the Claude Code or Codex plugin | [Plugin guide](https://github.com/LinVireo/browsertap-mcp/blob/main/docs/PLUGINS.md). | | Diagnosing a local setup | [Troubleshooting](https://github.com/LinVireo/browsertap-mcp/blob/main/docs/TROUBLESHOOTING.md), with the output of `browsertap doctor`. | | An agent calling BTAP tools | The client's live tool schemas and the optional [caller skills](#agent-skills-optional). | | A human or agent changing BTAP | [Contributing](https://github.com/LinVireo/browsertap-mcp/blob/main/.github/CONTRIBUTING.md); coding agents also read [AGENTS.md](https://github.com/LinVireo/browsertap-mcp/blob/main/AGENTS.md). | The [Tools](#tools) section is the complete parameter reference for this source tree. For an installed release, use documentation from its matching tag; development checkouts can contain unreleased changes. Runtime capabilities come from the connected server, not from a different version of the README. ## Key features - **Read and inspect pages:** simplified HTML/text, JavaScript, screenshots, and bounded network/console captures. - **Interact in a background tab:** `page_click`, `page_type`, `page_press`, and `page_drag` use trusted CDP input without moving the desktop cursor. - **Fill embedded forms:** locate, wait for, click, and type into controls in nested same-origin and cross-origin iframes. - **Use the existing profile:** authenticated downloads, cookies, storage, bookmarks, extensions, and temporary site-permission leases. - **Control interruptions:** explicit dialog policies, condition-based waiting, and operation handles for collecting delayed results without replaying an action. - **Separate concurrent tasks:** explicit browser/tab targets and owner-aware cleanup. Different tabs can run concurrently; shared-profile state is not isolated. - **Connect multiple browsers:** one bridge can serve Chrome, Edge, Opera, and multiple profiles. Browser-level operations can work without a page tab. ## Common workflows - **Read authenticated sites:** summarize a selected page, extract a table, or follow links using your existing login. - **Complete web forms:** search, filter, and fill controls in ordinary pages, SPAs, and embedded frames. - **Collect files and inspect pages:** download reports, capture screenshots, and inspect network requests or console messages. See the [usage guide](https://github.com/LinVireo/browsertap-mcp/blob/main/docs/USAGE.md) for step-by-step workflows and the [tool reference](#tools) for parameters.
Capability model and tool results BTAP exposes three capability layers so an agent can choose the narrowest interface that matches the task: - **Page capability** — DOM inspection, JavaScript, waiting, scrolling, screenshots, and `page_*` CDP input inside a named tab. This is the default path for ordinary web workflows and does not use the operating system mouse or keyboard. - **Browser capability** — the real Chromium profile and its browser-native surface: tabs, downloads, cookies, storage, permissions, bookmarks, extensions, service-worker messaging, and raw CDP. These operations can work without a foreground tab. - **Desktop capability** — explicit inspection and cancellation attempts for a foreground Windows standard file dialog owned by registered Chrome or Edge. Call `inspect_native_file_dialog` first, then use its short-lived ticket with `cancel_native_file_dialog`; both require `desktop_opt_in=true` and `[desktop]`. Other browser UI and unsupported native layouts remain outside this surface. The seven global OS input/screenshot tools removed in 0.5.0 remain removed. `resolve_leave_dialog` remains a page-scoped, lab-only recovery workflow; its final Enter fallback is a restricted exception, not a general desktop surface. `get_setup_status` returns tool counts and capability groups in `data.capability_registry`. MCP `tools/list` supplies the actual tool schemas and explicit `readOnlyHint`, `destructiveHint`, `idempotentHint`, and `openWorldHint` for every tool. These describe all supported parameter paths: tools with optional script execution, buffer clearing, or file writes are not classified as read-only. `get_setup_status` can tighten an existing Windows token file's ACL and is also classified as state-changing. Annotations help hosts plan calls; they do not grant permission or replace BTAP's ownership and concurrency checks. Every public tool now returns the `btap.result.v1` envelope without renaming tools: successful operation data is in `data`, explicit legacy failure payloads remain in `legacy`, and `error`/`error_code`, `retryable`, `target`, and `diagnostics` provide stable machine-readable status. Failures also set MCP `isError=true`. Arrays, objects, HTML, and other large bodies are read from `data` or `legacy`; only small scalar operation fields are projected at the top level for compatibility. Use the envelope's retry verdict: an explicit `retry_safe=false` or possible execution overrides a connection error's usual retry hint.
## Requirements - Python 3.10+ - Chrome, Edge, or Opera - Linux, macOS, or Windows. The ordinary page, browser, and CDP tools do not require OS-level input. The lab-only `resolve_leave_dialog` fallback and the Windows native-file-dialog tools need a usable desktop session. - A running Chromium user session with the BrowserTap Bridge extension loaded. - Claude Code, or any other MCP client ## Getting started ### 1. Install Create a virtual environment and install the package. The optional `desktop` extra is needed for the lab-only physical fallback or explicit native-file-dialog tools: **Windows PowerShell** ```powershell python -m venv .venv .\.venv\Scripts\python.exe -m pip install browsertap-mcp .\.venv\Scripts\browsertap.exe extension-path ``` **Linux or macOS** ```bash python -m venv .venv ./.venv/bin/python -m pip install browsertap-mcp ./.venv/bin/browsertap extension-path ``` The core install (`pip install browsertap-mcp`) is sufficient for page, browser, and CDP tools. It omits `pyautogui`, `mss`, and `pillow`, which are used by the lab-only `resolve_leave_dialog` Enter fallback and its screen/input checks. The explicit Windows native-file-dialog tools also require `[desktop]`. To work on the project rather than only use it, install the checkout as editable instead. Same extras; the extension directory and the skills are then read straight out of the tree: ```bash git clone https://github.com/LinVireo/browsertap-mcp.git cd browsertap-mcp python -m venv .venv ./.venv/bin/python -m pip install -e ".[dev,desktop]" ./.venv/bin/browsertap extension-path ``` ### 2. Load the Chrome extension This project ships an unpacked extension that has to be loaded once by hand. ```bash browsertap extension-path ``` Open `chrome://extensions`, turn on **Developer mode**, click **Load unpacked**, and pick the directory that command printed. The loaded extension is listed as **BrowserTap Bridge**. If you also use Edge or Opera, repeat the same steps at `edge://extensions` or `opera://extensions` with the same directory. The bridge tells the browsers apart automatically. Then open a normal `http://` or `https://` page. A blank tab is not enough — content scripts cannot run on `about:blank`, so no session is established. #### Connection status badge The extension may show a small `BTAP: checking`, `BTAP: connected`, or `BTAP: disconnected` badge on pages. The badge is presentation-only: it reports the bridge connection state and does not display page content, cookies, tokens, or URLs. Open the extension popup and clear **Show connection status on pages** to hide it. Hiding the badge does not stop the bridge, keepalive, or automatic reconnect behavior. #### The extension popup The popup holds the badge toggle above and two buttons, both of which act on the tab you are looking at: | Control | What it does | |---|---| | **Refresh** | Lists that tab's cookies **including their values, and including `HttpOnly` ones** the page's own JavaScript cannot read. It is a debugging aid, so it deliberately shows what a `document.cookie` dump would hide. | | **Copy** | Writes every listed cookie to the system clipboard as `name=value` lines. | Treat both as handling live credentials: anything that later reads your clipboard receives session cookies, and a screenshot of the popup captures them. See [SECURITY.md](https://github.com/LinVireo/browsertap-mcp/blob/main/.github/SECURITY.md) for where this sits in the threat model. ### 3. Add the server to your client **Standard config** works in most tools: ```json { "mcpServers": { "browsertap": { "type": "stdio", "command": "browsertap" } } } ``` If you installed into a virtualenv, point `command` at the executable's absolute path instead — relying on `PATH` is the most common reason a client fails to start the server.
Claude Code ```bash claude mcp add browsertap -- browsertap ``` Add `--scope user` to make it available across all projects. For a virtualenv install: ```bash claude mcp add browsertap -- /absolute/path/to/.venv/bin/browsertap ``` On Windows PowerShell, use the absolute path to `.venv\Scripts\browsertap.exe` instead. Verify with `/mcp`.
Claude Desktop Follow the MCP install [guide](https://modelcontextprotocol.io/quickstart/user) and use the standard config above. An example file is included at `examples/claude-desktop-config.json`.
Cursor Put the standard config in `.cursor/mcp.json` for one project, or `~/.cursor/mcp.json` globally. An example file is included at `examples/cursor-mcp.json`.
VS Code ```bash code --add-mcp '{"name":"browsertap-mcp","command":"browsertap"}' ``` Or write it into `.vscode/mcp.json` by hand — note that VS Code's key is `servers`, not `mcpServers`.
Hermes Add to `~/.hermes/config.yaml`: ```yaml mcp_servers: browsertap: command: browsertap timeout: 120 connect_timeout: 60 ``` `browsertap print-hermes-config` prints this snippet. An example file is included at `examples/hermes-config.yaml`. Verify with `hermes mcp list`.
Other clients Any MCP client that speaks stdio will work. Follow its own install guide and use the standard config above.
### Your first prompt Once the extension is loaded and a normal page is open, try: > What tabs do I have open? Read the current page and summarise it. If tabs come back empty, run `browsertap doctor`. For the least disruptive workflow, start with [`docs/USAGE.md`](https://github.com/LinVireo/browsertap-mcp/blob/main/docs/USAGE.md): it explains which operations stay in a background tab, when foreground activation is needed, how the remaining physical fallback is gated, and when an image-capable model is useful. ## Configuration ### Environment variables | Variable | Default | Purpose | |---|---|---| | `BROWSERTAP_BRIDGE_HOST` | `127.0.0.1` | Bridge bind address. | | `BROWSERTAP_BRIDGE_PORT` | `18765` | Integer from `1` through `65533`. WebSocket uses this base, HTTP uses `PORT+1`, and the host lock uses `PORT+2`. Invalid values fail before network or spawn actions. The separate `spawn.lock` file prevents concurrent daemon starts. For a custom port, also configure the extension — see [docs/TROUBLESHOOTING.md](https://github.com/LinVireo/browsertap-mcp/blob/main/docs/TROUBLESHOOTING.md). | | `BROWSERTAP_STATE_DIR` | `~/.browsertap` | Override the state directory. A nonempty relative path is anchored to the launching process's working directory before daemon spawn. Existing legacy-directory fallback remains available when no override is set. | | `BROWSERTAP_NO_SPAWN` | unset | Set to `1` to stop the MCP server from auto-starting the bridge. Use it when you run the bridge yourself. | | `BROWSERTAP_BRIDGE_AUTH` | enabled | Set to `off` only for an explicitly trusted local compatibility setup. By default BTAP authenticates `/link` with a persistent per-user token. | | `BROWSERTAP_BRIDGE_TOKEN_FILE` | `~/.browsertap/bridge-token` | Override the shared token file location; relative paths use the launching process's working directory. Without this override, the token lives in the selected state directory. Editors do not need individual tokens. | | `BROWSERTAP_BRIDGE_TOKEN` | unset | Legacy one-time migration source. If the token file does not exist, BTAP imports this value once; the file wins thereafter. | | `BROWSERTAP_PREFERRED_BROWSER` | unset | `chrome`, `edge`, or `opera`. Which browser wins when several are connected and no tab is specified. | | `BROWSERTAP_MODE` | `lab` | `lab` prioritizes uninterrupted automation and skips physical-input/site-allow elicitation; `safe` prompts for every such action. `set_automation_profile` changes only the current MCP process. | | `BROWSERTAP_ALLOW_UNSAFE_CDP` | unset | Raw CDP blocks the high-risk methods listed in [SECURITY.md](https://github.com/LinVireo/browsertap-mcp/blob/main/.github/SECURITY.md). `1` permits them only in `lab`; `safe` retains the guard. Other allowed methods can still change page or profile state. | | `BROWSERTAP_LAB_NO_ELICIT` | enabled | Lab skips elicitation by default. Set this to `0`/`false` only when you want session-level lab approval prompts; the cross-process lock, quiet-input gate, foreground confirmation, and ownership checks always apply. | | `BROWSERTAP_AUTO_BEFOREUNLOAD_HOSTS` | `shell.,ttyd,code-server,jupyter,vscode-web` | In lab, ordinary `open_url` accepts beforeunload on matching current hosts. `intent_leave=false` always preserves the page. | | `BROWSERTAP_WS_ALLOWED_ORIGINS` | unset | Comma-separated exact extra origins for WebSocket and HTTP. The packaged extension ID is pinned by default, from its manifest key or unpacked path; separate copies need their own explicit Origin. HTTP token authentication still applies, including without `Origin`. | | `BROWSERTAP_WS_ALLOW_NO_ORIGIN` | unset | Set to `1` only for a trusted non-browser local WebSocket client that cannot send `Origin`. The default rejects origin-less clients. | ### CLI ```bash browsertap # run the MCP server (stdio) browsertap --version # print the installed package version browsertap extension-path # print the unpacked extension directory browsertap skill-path # print the directory holding the shipped agent skills browsertap doctor # diagnose the local setup, as JSON browsertap bridge # run the bridge in the foreground browsertap bridge --restart # restart the managed bridge; does not touch the browser browsertap bridge --stop # stop the exact managed bridge process browsertap print-hermes-config # print a Hermes config snippet ``` `doctor` reports the extension path, port state, and connected tab count. It also returns a structured verdict: `cause` is one of `healthy`, `starting`, `ext_never_registered`, `sw_slept_or_dropped`, `registering`, or `bridge_unreachable`, and `advice` is the matching one-line fix. `starting` means the bridge has just started and is waiting for the extension handshake; its `action` is `wait_for_extension`, so wait a few seconds and run `doctor` again. `registering` means the extension is connected but no normal `http(s)` content tab is ready. When no extension runtime status is available after startup, the setup status is `extension_unavailable` with `action: check_extension_connection`. Check that BrowserTap Bridge is enabled in the intended browser and retry `doctor`. `extension_status_available=false` means compatibility is still unknown; missing runtime data alone does not request an extension Reload. Confirmed old bridge or MCP versions retain their own restart action, including during startup. `doctor` prints JSON on stdout even for invalid configuration. An invalid base port reports `status: "initialization_failed"`, `action: "check_config"`, `error` and `error_type` before network or spawn actions. Imports, help/version and package-path commands remain usable. DNS/socket failures during later port probes preserve the available setup diagnosis and add `port_probe_errors`; an affected port's state is null. Exit status is `0` only for `healthy` or `starting` without probe errors. A malformed bridge diagnosis reports `cause: "bridge_unreachable"`, `ok: false` and `error_code: "malformed_diagnosis"`; it does not establish that the bridge is an old build. `state_paths.token_file_status` distinguishes `missing`, `empty`, `ready`, `unreadable` and `invalid_encoding`. `token_file_error` gives a safe reason; only ready content has a fingerprint. Existence fields are null when metadata cannot be read. Unknown default-directory metadata keeps the canonical path; legacy selection requires the default directory to be confirmed absent. Existing empty or unreadable files are not overwritten. `mcp_build_verdict` and `bridge_build_verdict` compare each process's package-import source snapshot with disk: `matches_tree`, `stale_process`, or `unverifiable`. The snapshot includes package Python files and import-cached JavaScript used for result conversion, guarded execution, scoped dialogs and page inspection. Same-version edits still require the corresponding MCP or bridge restart. Missing assets or older identity schemas cannot prove a match. This source identity does not attest runtime monkeypatches, cached bytecode or arbitrary other assets; `*_build_enforced=false` is unknown. BTAP creates `~/.browsertap/bridge-token` on first use and every bridge/MCP process reads that same file. Closing browsers or editors does not rotate it. Removing the browser extension or reinstalling the Python package deliberately leaves the token file in place, so a reinstall continues to work. A full user-data purge may delete the whole `~/.browsertap` directory only after all BTAP bridge processes have stopped; the next start then creates a new token. ### Agent skills (optional) The Claude Code and Codex plugins load these Skills automatically. This section is for clients configured through the standard MCP installation. BTAP includes the primary workflow and recovery rules in MCP initialization instructions and tool descriptions. The same two packaged Markdown guides are also discoverable through MCP `resources/list` and readable with `resources/read`: `browsertap://agent/workflow` and `browsertap://agent/recovery`. A client can read them without installing Skills. Hosts decide when to load resources; the critical target, ownership and retry rules remain in the instructions and tool results. ```bash browsertap skill-path # e.g. .../site-packages/browsertap_mcp/skills ``` That directory contains: | Skill | What it is for | |---|---| | `browsertap-default/SKILL.md` | The calling contract: pick a target before acting, open your own tab for anything that mutates a page, close it in cleanup, and how to react to `no_response` / `switched_session` / `bridge_error`. | | `browsertap-bridge-recovery/SKILL.md` | Recovery when the transport itself is down: which of the three components is stale, and the one restart or reload that fixes it. | These are instructions for an agent using BTAP, not for an agent editing BTAP's source. Coding rules and test commands belong in [AGENTS.md](https://github.com/LinVireo/browsertap-mcp/blob/main/AGENTS.md) and [Contributing](https://github.com/LinVireo/browsertap-mcp/blob/main/.github/CONTRIBUTING.md). Point your client's skill manager **at that directory** rather than copying the files. A copy looks correct for as long as the contents happen to agree, then silently stops receiving updates when you upgrade the package. If you keep copies anyway, `python -m scripts.check_tool_docs --check-installed-skills --skill-mirror DIR` compares them against the shipped originals and names whichever one drifted. ### Upgrade Plugin users: follow [plugin updates](https://github.com/LinVireo/browsertap-mcp/blob/main/docs/PLUGINS.md). The steps below apply to a Python package installation. The marker below is maintained with this source tree. It is not proof that a development checkout has been published; compare the installed package with its release tag before using new tool signatures or the 0.5.0 migration notes. Current release: unified Python package, bridge, and unpacked Chrome extension **0.5.4**. The three components load updates separately: 1. Update the installed package with `pip install -U browsertap-mcp` (keep `[desktop]` if you use that extra), then restart its MCP session. 2. Run `browsertap doctor`. If it requests `restart_bridge`, use `browsertap bridge --restart` to load the daemon's updated code. 3. If it requests `reload_extension`, open `chrome://extensions` and press **Reload** on BrowserTap Bridge. Extension source changes require that manual step. `browsertap doctor` reports which part is stale and names the one action that fixes it: `reload_extension`, `restart_bridge`, or `restart_mcp_session`. The other two will not help, so read the field rather than doing all three. ### Uninstall 1. Stop the managed daemon with `browsertap bridge --stop`. 2. Open `chrome://extensions` (or the equivalent page in Edge/Opera) and remove the unpacked **BrowserTap Bridge** extension. 3. Remove the `browsertap` entry from each MCP client's configuration. 4. Run `pip uninstall browsertap-mcp` in the environment where it was installed. If you created a dedicated virtual environment, remove that specific environment after deactivating it. 5. Optional full cleanup: after confirming every BTAP bridge is stopped, remove `~/.browsertap`. This deletes the persistent bridge token and logs; the data is retained by default so reinstalling continues to work without reconfiguration. ## How it works Three layers: 1. **Chrome extension** (MV3) — injected into real pages, reaches `tabs`, `cookies`, `debugger`, and `management` through Chrome APIs. 2. **BrowserBridge** — a local daemon on `127.0.0.1:18765` (WebSocket) and `:18766` (HTTP). It owns the extension connections, tracks sessions, and relays results. It runs detached from any MCP instance, and the MCP server starts it on demand with no console window. `client_id` identifies one connected browser/profile instance; `session_id` is its current composite `clientId:tabId` handle, not a permanent tab identity. Connected rows may also carry `tab_identity`. Several browsers and profiles coexist. 3. **MCP server** — exposes the whole thing as MCP tools. Each agent's MCP process keeps its own selected target and tab ownership registry. Two channels reach the browser: a per-tab session channel, and a direct channel to the extension's service worker. The second one is why some tools keep working when every tab is closed. ## Behaviour you should know before driving it **Selecting a tab does not raise it.** `switch_tab` defaults to `activate=false`: it only changes which tab later calls target. Nothing moves on screen until you call `activate_tab`, pass `switch_tab(activate=true)`, or approve a physical-input action. Page reading, JS, and the `page_*` input tools all work on a background tab. **One coordinate space, inside the tab.** `page_click`/`page_drag` take **viewport** coordinates inside one tab and are dispatched through CDP — no cursor movement, no window focus, `foreground_changed: false` in the reply. There is no desktop-coordinate tool to confuse them with any more: the ones that took physical screen pixels were removed in 0.5.0. **Two pixel units, and the screenshot does not use the one you click with.** Viewport coordinates are **CSS pixels** — the space `getBoundingClientRect` reports. A page screenshot comes back in **device pixels**, which is CSS × `devicePixelRatio`, so at 125% display scaling a point read off the picture is 25% too large for `page_click`; `capture_page_screenshot` reports `image_width`/`image_height` and `pixel_space: "device"` so the factor is visible instead of assumed. Reading a point off a picture is the one path with no hit test — prefer a `scan_page` selector, which is checked against the page before anything is dispatched. **Automation profiles.** With `BROWSERTAP_MODE` unset, BTAP defaults to `lab` with `BROWSERTAP_LAB_NO_ELICIT=1` semantics. Lab permits site `allow` and the restricted leave-dialog fallback without elicitation; `safe` prompts for each site `allow` and refuses the physical Enter fallback. Neither profile is a confirmation prompt for every browser action. Ownership and target checks still apply, and the physical path keeps its OS lock, quiet-input gate and `on_screen` check. `input_quiet.enforced` says whether comparable input markers were available. **Raw CDP.** `get_automation_profile.raw_cdp_policy` reports `guarded` or `allow_unsafe`. Single commands and entire batches are checked before dispatch; use dedicated tools for tab closure, cookies, permissions and user-agent changes. The guard covers common destructive methods, while allowed JavaScript/CDP can still change a page. The complete policy and explicit lab override are in [SECURITY.md](https://github.com/LinVireo/browsertap-mcp/blob/main/.github/SECURITY.md). **Dialogs are explicit.** `execute_js(dialog_policy=...)`, `open_url(beforeunload=...)`, and `handle_dialog(action=...)` take `dismiss` (default), `accept`, or `manual`. The global default still preserves the page; only an explicit accept or lab's configured shell/IDE host heuristic leaves automatically. `handle_dialog` answers within three seconds or reports `no_dialog`/an explicit error. `resolve_leave_dialog` tries protocol accept twice and uses physical Enter only as a final, lab-approved fallback. The extension route for `execute_js` prepares the current injectable frames for `accept`/`dismiss` before running the caller and restores temporary helpers on completion or expiry. The Python CDP fallback for an older command router covers only its current evaluation context. New documents do not inherit either scope. Preparation shares the command deadline; a caller's CSP-like error or an uncertain result never justifies replaying it. **Permissions use temporary leases.** `set_site_permission` covers one origin for 60–600 seconds, records the prior setting, and attempts restoration on expiry/reset/service-worker restart. `safe` prompts for every `allow`; default `lab` applies it without elicitation. Unsupported restoration is retained as `manual_recovery` with the prior setting and recovery guidance, and automatic retries stop. Correct the cause before an explicit `reset_site_permissions` retry. Unsupported initial grants return `unsupported` or `requires_user_action`. **Challenges stay in your browser.** A Cloudflare Turnstile or similar widget is handled in the same connected tab, by `page_click`, with a bounded number of attempts. When the challenge has not moved, the result is `challenge_stalled` and BTAP stops so you can finish it yourself in that same tab. BTAP never launches Playwright, a headless browser, or a separate automation profile as a fallback — the whole point is your real, logged-in session. **Changed tools need a reload.** Tool schemas and descriptions are read once when your client starts the MCP server; after upgrading, restart the MCP session or your client, or you will keep calling the old signatures. Extension changes need a manual reload at `chrome://extensions` — `chrome.runtime.reload()` restarts the service worker without re-reading the files from disk. ### Tab ownership in concurrent tasks Classify every tab before using it. A **U (user) tab** existed in the first `list_tabs` snapshot; do not close it or navigate it by default. An **A (agent) tab** is created by this task's `open_new_tab`; save its `session_id`, `generation`, and `owner_id`, pass that explicit session to every operation, and call `close_tabs(..., owner_id=...)` in cleanup. A **B (borrowed) tab** is a temporarily used U tab; record its `original_url` and never close it. Restore a URL changed by this task only after confirming the same tab lifecycle still exists and the user has not since navigated it elsewhere. Decision order: run `list_tabs`; borrow an existing match only for read-only/light work; open an A tab for searches, filters, sorting, pagination, scrolling, expand/collapse, navigation, forms, or other actions that change the page view or state; open an A tab when no match exists; finally close only A tabs. Never register the initial tab snapshot as owned, close a U/B tab, omit the explicit target for state changes, reuse an old native tab id, omit generation-aware cleanup, or leak an A tab. For parallel agents, use **a separate A tab per agent and explicit `session_id` on each call**. Different tabs can run concurrently both within one MCP process and across independent MCP processes. Each process has its own default tab; agents sharing a process also share that default, so `switch_tab` is not an agent identity. Each call snapshots its target, and an explicitly targeted call does not change another call's target or the process default. A cooperative target lock covers each complete MCP call and its internal browser roundtrips. A competing call to the same tab returns `target_busy`. The bridge also records reservations for dispatched commands: `wait=false` and some timeout paths retain the reservation pending a definitive browser result, a confirmed tab lifecycle end, or the bounded retention expiry described below. Claim an `execute_js` result with `get_execute_js_result` from the same MCP session; do not replay a script whose result is pending. Direct `/link` and Python driver calls receive the bridge's per-command reservation, but need an MCP command scope for the lock across multiple browser roundtrips. A script deadline, debugger timeout or detach can leave JavaScript running. Dispatched `exec_timeout` replies retain the same bounded reservation as other uncertain outcomes: `status=in_progress`, `operation_status=outcome_unknown`, and `reservation_held=true`. After that retention window, the receipt stays unknown and `retry_safe=false`. Neither a failed receipt nor an expired reservation proves execution stopped; avoid replay or conflicting work in that tab while the outcome is uncertain. Manual dialogs keep their reservation, and only the originating MCP session can call `handle_dialog`. Handling a dialog does not prove the script finished: when the extension cannot return its final result, the operation remains `outcome_unknown`. The caller can close its own tab with `close_tabs(..., owner_id=...)` to end that lifecycle. Console and network capture mutations belong to the MCP session that started them. Another session cannot restart, stop, or clear that capture (`capture_busy`); ordinary page operations and non-clearing console reads remain available. After the owning MCP process has exited, another session can reclaim its capture. Process exit alone does not cancel page JavaScript or release a pending target; the tab owner can close its own tab to recover. Agents sharing one MCP process share this ownership too. These guards do not make a multi-step workflow atomic or isolate cookies/storage shared by a profile. ### Structured statuses and recovery fields Read `ok` and `error_code` first; operation statuses remain in `data`/`legacy` and as small top-level compatibility fields. Failures set MCP `isError=true`: | `status` / `error_code` | Meaning | |---|---| | `ok` / `success` | Completed and verified as far as the protocol allows. | | `redirected` | Navigation landed on a different URL than requested (login wall, SSO, canonical rewrite). | | `navigated` | An `execute_js` script navigated the page, so its return value is genuinely gone; `landed_url` says where it went. | | `blocked_by_dialog` | A JavaScript dialog is open and waiting for `handle_dialog`. | | `blocked_by_beforeunload` | Navigation was cancelled to keep the page; re-issue with `beforeunload="accept"` to leave. | | `dialog_handle_failed` | A dialog was seen but answering it failed; the tab may still be blocked. | | `navigation_failed` / `navigation_timeout` | `open_url` did not complete within its timeout, or the browser reported an error. | | `triggered` with `type="download"` | `open_url` was replaced by a browser download. `ERR_ABORTED` can be normal only when CDP also reports `isDownload=true`; use `download_file` for completion and the local path. | | `requires_user_action` | The action needs user intervention; approval failures include `reason` (`elicitation_unsupported`, `declined`, `timeout`, `cancelled`, or `error`) and do not execute the action. | | `raw_cdp_blocked` | A raw method bypasses a protected state/ownership path. No command was dispatched; use a dedicated tool or resolve the operator configuration instead of retrying unchanged. | | `busy` | Another BTAP process holds the physical-input lock, or the tab already has a pending manual execution. Returned immediately, never queued. | | `target_busy` | This tab is reserved by another call or a still-pending browser command. Check `delivery_state` and `retry_safe`; a call involving multiple tabs may have completed earlier steps. | | `capture_busy` | Another MCP session owns this console/network capture. Its owner must stop it before another session can restart, stop, or clear it. | | `ambiguous_browser` | Several browser/profile instances match and no unique browser was selected. Use `list_tabs`, then pass the chosen full `session_id` (or `client_id` where supported). | | `input_activity_detected` | You used the mouse or keyboard during the post-approval quiet window, so no physical input was sent. | | `activation_failed` | The target tab could not be confirmed on screen, so no physical input was sent. | | `unsupported` | The browser or extension API cannot provide this (e.g. clipboard permission leases). | | `challenge_stalled` | A browser challenge made no progress within the attempt bound; hand the tab back to the user. | | `no_response` | The script did not reach the tab or timed out — do not blindly retry anything with side effects. | | `not_found` | The selector matched nothing; no input was dispatched. | | `bridge_error` | A bridge call failed. It may appear as `error_code` or a diagnostic field rather than the top-level status; run `list_tabs`/`doctor` before retrying. | | `switched_session` | Supplemental field indicating that only an implicit dead default was replaced with another live tab. Verify the new target before continuing; explicitly directed dead sessions are never substituted. | For delivery failures, only `delivery_state="undelivered"` proves the operation was not sent; `retry_safe=false` still prevents replay. `sent_unconfirmed` means its ACK or HTTP response is missing, so it must not trigger an automatic replay. Treat `delivered_no_result`, `navigated`, and unknown delivery as potentially executed; recover by operation ID when one is available and inspect the page before deciding the next action. ### Complete JSON results Large JS values and strings containing unpaired UTF-16 code units are returned through `result_file`. When `result_file_encoding="json"`, first parse the path field once as JSON; otherwise use the path directly. Then read that file as UTF-8 JSON and verify `result_bytes` and `result_sha256` against its bytes. The path encoding is separate from the file contents. `result_file_scope` identifies those contents: | Scope | JSON contents | | --- | --- | | `js-value` | The complete converted JavaScript value; its inline value is null. | | `envelope` | The complete original v1 tool envelope. | | `mcp-call-result` | The complete native MCP result after normal envelope adaptation, including original content and metadata. | JS descriptors are in `data`, or in `legacy.late_result` for a late reply. Unrepresentable strings elsewhere in a tool result use the latter two scopes and retain a valid structured decision header with the original `ok`, `isError` and retry verdicts. `result_content_externalized` / `result_meta_externalized` identify native content or metadata moved into the archive. If the file cannot be written, parse `result_json` once as JSON; `result_json_scope` uses the same scope names. This explicit inline fallback can exceed the usual size limit and keeps the original operation receipt. For envelope/MCP scopes it is at the header root, with a `result_json_ref` in `data`/`legacy`. `result_file_error.message_json` is a JSON-encoded I/O message. An error field marked `message_encoding="json"` or `code_encoding="json"` (also `error_code_encoding`) must likewise be parsed once. These representations preserve the original UTF-16 values; export failure never permits replay. ## Disclaimers This server exposes your real browser profile to the connected MCP client, including logged-in sessions. Its scope is browser automation, with only the restricted physical leave-dialog fallback described below. - One physical-input path is left after 0.5.0: `resolve_leave_dialog`'s Enter fallback, `lab` only, and only after two protocol-level attempts fail. It is real OS-level input rather than a synthetic page event, so it lands on whatever is on screen; `safe` refuses to send it at all. The `page_*` tools carry none of this exposure. - Page content is untrusted input. A page your agent reads can attempt prompt injection, and the tools available make that consequential. - This is **not** a security boundary. See [MCP Security Best Practices](https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices). - Avoid pointing it at sensitive accounts you would not want an MCP client to see, and prefer not to run it on shared or production machines. The extension requests broad permissions because the feature set requires them: `cookies`, `tabs`, `debugger`, `scripting`, `alarms`, `storage`, `contentSettings`, `declarativeNetRequest`, `management`, `bookmarks`, `downloads`, and ``. `declarativeNetRequest` temporarily removes CSP response headers only from the tab executing an eval-based command. The rule is session-scoped, reference-counted, and removed in cleanup; it is not a browser-wide persistent CSP override. See [Security](https://github.com/LinVireo/browsertap-mcp/blob/main/.github/SECURITY.md) for the full permission and loopback threat model. ## Tools Most tools accept an optional `session_id` to target one specific tab; omitting it uses this MCP process's current target. Pass it explicitly for state changes. `client_id` distinguishes connected browser/profile instances; `session_id` is a composite handle such as `chrome_a1b2c3:456`. Pass returned handles verbatim. With multiple instances connected and no browser selected, routing returns `ambiguous_browser`. Select a full `session_id` from `list_tabs` with `switch_tab`, or supply it to the operation; `open_new_tab` also accepts `client_id`. `browser="chrome"` alone is insufficient when multiple Chrome profiles match. If Chrome reports an evidence-backed replacement for the same tab, BTAP returns `rebound_from`, `replacement_session_id`, and `tab_identity`; otherwise an explicit stale handle is refused. Tools marked **no tab needed** use the extension's service worker and work with zero tabs, but still need a unique browser/profile selection.
Tabs and navigation - **get_setup_status** — report `package_version`, `bridge_version`, `extension_version`, `protocol_version`, connection state, ports, tabs, and the required recovery action. A missing bridge listener is started automatically when spawning is enabled; `restart_bridge_required=true` means a bridge that is still running must be replaced with `browsertap bridge --restart`. `reload_extension_required=true` identifies the unpacked-extension platform limit and requires a manual Reload; a version number that differs on its own no longer sets it, because Chrome parses `manifest.json` at load time and never re-parses it without a Reload, so a release bump would otherwise demand a click whose only effect is on that number. `restart_mcp_session_required=true` is the opposite direction: a component is *newer* than the running server, so the stale build is this process and only restarting the MCP session or client clears it — the other two flags stay false, because a restart or reload would report the same mismatch again. `extension_build_stamp` is the stronger signal and answers the question the four version fields cannot: it is a hash of the extension sources compiled into `background.js`, reported by the worker actually running, so comparing it to `expected_extension_build_stamp` (a fresh hash of the directory) is decisive in both directions where version equality was measured wrong twice. Read the answer from `extension_build_verdict`: `matches_tree` (the worker is running this code), `stale_worker` (it is not -- Reload), `stamp_not_regenerated` (an extension file was edited without running `python -m scripts.extension_stamp --write`, so the comparison proves nothing either way) or `unverifiable` (the extension predates the stamp, or the directory could not be read -- see `extension_build_error`). `extension_build_enforced=false` means no comparison happened, so treat it as unknown rather than as a pass. Answers while another tool is still running; `default_session_id` is this request's snapshot of the MCP process default and `default_session_settled=true` because another call's temporary target is isolated. No parameters. `extension_status_available=false` means the extension has not supplied runtime status: `starting` asks to `wait_for_extension`, and `extension_unavailable` asks to `check_extension_connection`. Missing status alone does not request a Reload. Compatibility checks, including legacy replies missing required fields, apply once runtime status is available. On Windows, inspecting an existing token file can tighten its ACL to the current user. The token-file state check itself does not create a missing file; `get_setup_status` may still create one during bridge startup or authentication initialization. - **get_automation_profile** — inspect whether the current MCP process uses `lab` or `safe`. - **set_automation_profile** — switch the current MCP process between `lab|safe`; the override is not persisted and does not reload the extension. - `mode` (string): `lab` or `safe` - **list_tabs** — list connected tabs under `data.tabs`, including their full session handles and `browser` fields. Answers while another tool is still running; `default_session_id` is this request's snapshot of the MCP process default and `default_session_settled=true`. Parallel agents should still pass an explicit target. No parameters. A timed-out inventory releases that read's bridge reservation. Pending mutations keep their reservations; a release does not establish whether another operation happened. - **list_all_tabs** — *(no tab needed)* list every open tab, including `chrome-extension://` pages that `list_tabs` hides. Those never become sessions, so they have no session id; drive them with `cdp_command(tab_id=...)`. - `session_id` (string, optional): which browser/profile to ask. - **switch_tab** — set this MCP process's *target* tab for later calls. A `url_pattern` must match exactly one tab; if several match, select one with its full `session_id`. A `browser` filter matching multiple profiles also requires an explicit `session_id`. It does **not** raise the tab or focus the browser: `activate` defaults to `false`. Pass `activate=true`, or call `activate_tab`, when you need the tab in front. - `session_id` (string, optional), `url_pattern` (string, optional): substring match, `browser` (string, optional): `chrome`, `edge`, or `opera`, `activate` (boolean, optional): default `false`. - **activate_tab** — bring a tab to the foreground and focus its window. This is the explicit way to raise a tab, and the only one that does not involve approving physical input. Check `on_screen` in the reply: BTAP first asks Windows to restore a minimised browser, but `on_screen=false` means visibility still could not be confirmed and screen-coordinate input must not be sent. - `session_id` (string, optional) - **open_url** — navigate the current tab. Global behavior remains `dismiss`; lab automatically accepts beforeunload on configured shell/IDE hosts. If the extension's `navigate` route is unavailable on a heavy SPA, BTAP falls back to `Page.navigate`. A CDP result with `isDownload=true` returns `{type:"download",status:"triggered"}` instead of only `navigation_failed`; the accompanying `ERR_ABORTED` is normal for that download navigation. - `url` (string), `session_id` (string, optional), `timeout` (number, optional): default `15`, `beforeunload` (string, optional): default `dismiss`, `intent_leave` (boolean, optional): `false` forces page preservation - **download_file** — download an HTTP(S) URL through Chrome's native download manager, using that browser profile's cookies and authenticated session. It waits by default and returns `status="completed"` plus a verified absolute `path`; interrupted downloads return `failed`, while a timeout or `wait=false` returns `in_progress` with `download_id`. An explicit `session_id` must still be live and is never replaced with another profile. Use this for attachments instead of page `fetch`. - `url` (string), `filename` (string, optional): relative download name, `directory` (string, optional): arbitrary absolute destination directory; creates parents, `wait` (boolean, optional): default `true`; `directory` requires `true`, `timeout` (number, optional): default 60 seconds, maximum 1800, `session_id` (string, optional): selects the browser profile, `overwrite` (boolean, optional): default `false`; an existing final destination raises an error unless explicitly `true`. If a directory download times out, `directory_applied=false`: the move is no longer tracked and Chrome may finish into its default download directory. - **open_new_tab** — open a background tab in this MCP process's selected browser/profile, unless `session_id` or `client_id` selects another. Creates a unique `operation_id` and waits a bounded time for exact session/generation registration; pass `active=true` for foreground work. Returns `{operation_id,tab_id,session_id,generation,ready,owned,opener,owner_id,load_status}`. The extension deduplicates by operation id. Ownership requires a completed record with exact `client_id+tab_id+generation`, even when `ready=false`; `ready` only reports immediate availability for session tools. Before create dispatch, registry uncertainty returns `status="unknown",may_have_created=false,retry_safe=true`; after dispatch, uncertainty returns `may_have_created=true,retry_safe=false`. With `may_have_created=false,retry_safe=true`, resolve the reported failure and retry without `operation_id`. To recover a dispatched create with `retry_safe=false`, pass the same `operation_id`, returned `client_id`, and `owner_id`: recovery reads the durable record without replaying `tabs/create`. A failed recovery probe preserves that uncertainty and owner capability. If the initial recovery probe finds no record, `reconciliation.resume_required=false` directs the caller to `list_tabs()` and inspection of that browser instead of another recovery call. Missing records, matching URLs, or unchanged tab counts cannot prove non-creation or ownership; keep the outcome unknown unless exact identity and task ownership resolve it. Keep `owner_id` for cleanup of registered task-owned tabs with their exact session/generation. Use this native API for reliable new tabs; page `window.open()` or anchor clicks may be blocked without a user gesture. - `url` (string), `timeout` (number, optional): default `15`, `active` (boolean, optional): default `false`, `session_id` (optional browser/profile selector), `owner_id` (optional capability to group several tabs under one task owner), `operation_id` (optional recovery handle), `client_id` (optional browser/profile client selector for creation or recovery) - A pending create left by a worker restart becomes terminal `unknown`. Bounded record retention also keeps a replay guard for retired operation IDs; a refused old ID does not prove non-creation. Follow `reconciliation.resume_required=false` with inspection of that browser, preserving unknown outcomes and exact ownership evidence. - A failed status probe may return `reconciliation.bridge_operation`. Its wire `operation_id` can be queried with `get_execute_js_result` in the same MCP session; the outer creation `operation_id` still belongs to `open_new_tab` recovery. The probe's `reservation_held=false` does not prove an earlier create was absent or safe to replay. - **close_tabs** — *(no tab needed)* accept native numeric tab ids or full `client:tabId` session ids, including `chrome-extension://` tabs. The default `only_if_agent_owned=true` requires the `owner_id` returned by `open_new_tab` and verifies the current lifecycle generation before closing, so pre-existing user tabs and another agent's tabs are refused. If the user already closed an owned tab, cleanup returns `status=already_gone, closed_by=user` without reusing its native id. An actual owned close returns `closed_by=agent`; an explicit unowned/operator override returns `closed_by=none` so it is not counted as task-owned cleanup. If `already_gone` is returned but a page with the same work is still visible, call `list_all_tabs` and verify URL/title before deciding whether a new session/generation should be closed; BTAP never auto-transfers ownership by URL. A Chrome `tabs.onReplaced` identity mapping is safe and also migrates the ownership claim. Set `only_if_agent_owned=false` only when the operator explicitly asked to close an unowned/user tab. - `tab_id`, `session_id` (optional browser constraint), `owner_id` (required by the safe default), `only_if_agent_owned` (boolean, default `true`)
Page reading and execution - **scan_page** — read the page as simplified HTML or text. Returns `links` mapping each `#rN` ref in the content to its absolute URL, and `offscreen` + `hint` when content was left outside the viewport. A background tab may report viewport height zero; ordinary DOM/text/API work still continues there, and only visual/layout fidelity requires explicit `activate_tab`. When the page can be probed, `render_state`/`content_ready` distinguish real content from a loading, hydrating, or shell-only SPA; retry or use `wait_for` before treating an empty shell as final content. `cutlist` (on by default) collapses long repeated lists and reports a CSS selector for each container it collapsed, derived from that container's own structure. The built-in scan does not write page attributes, ids, or `window` globals. Optional `extra_js` runs caller code and can modify the page or send requests. - `session_id` (string, optional), `text_only` (boolean, optional): default `false`, `cutlist` (boolean, optional): default `true`; collapse repetitive lists, `maxchars` (integer, optional): default `35000`, `instruction` (string, optional), `extra_js` (string, optional), `timeout` (number, optional): default `15`, `frame` (array, optional): non-empty path of CSS strings or structured frame locators, `max_targets` (integer, optional): default `80`, range `0`–`200` `observation.targets` adds current locators, control names, editability, rectangles and `recommended_tool` with a reason. Pass a returned `locator` unchanged as the `page_click` / `page_type` selector; re-observe after page changes. `observation.frames` lists uninspected child documents: pass an entry's `frame` back to `scan_page` to inspect it, including cross-origin/OOPIF frames. Open shadow roots are included; this is a bounded DOM observation, not a complete accessibility tree. `max_targets` caps controls and frame entries together, separately from the content budget; `0` disables enumeration and `truncated=true` means more targets or DOM nodes remain uninspected. Rectangles use the observed document's viewport CSS pixels and are not hit-tested. `verify_coordinate_target` recommends a screenshot to verify a canvas or zero-size target, not an automatic coordinate click. Disabled, inert and readonly controls have no recommended input tool; native selects report `select_existing_option`. Only top-document file inputs outside shadow roots recommend `upload_files`, using `selector=locator.css`; frame/shadow file uploads report unsupported. Frame scans reject `extra_js` and omit the parent's readiness probe; top-document `extra_js` preserves its execution semantics and reports target metadata unavailable. The optional built-in readiness probe releases its tab reservation on timeout. Missing `render` fields mean readiness is unknown; use `wait_for` for the intended control. - **wait_for** — wait until a condition holds, then return. Use this instead of polling `scan_page`, which re-serializes the whole DOM each time. The server schedules short synchronous page checks under one deadline, avoiding background-page timer throttling. Exactly one condition is required. `selector` accepts legacy CSS or the structured locator object described under background page input. Caller-provided `js` is evaluated repeatedly and can have side effects; use a read-only predicate. A timeout with `operation_id` retains a pending check. Timed-out selector/text/URL probes can release the tab without discarding that receipt: `reservation_held=false` permits another command, and `get_execute_js_result` in the same MCP session can collect the delayed reply. Caller-provided `js` stays reserved. When `reservation_held` is true or unknown, keep querying the original operation until it settles or releases its reservation. A pending probe is never replayed. - `selector` (string/object, optional): CSS or structured locator, `text` (string, optional): substring of body text, `url_pattern` (string, optional): regex on the URL, `js` (string, optional): expression to become truthy, `gone` (boolean, optional): wait for the condition to stop holding; default `false`, `timeout` (number, optional): default `15`, `session_id` (string, optional) - **wait_for_url** — wait for navigation to settle: blocks until the tab URL matches `url_pattern` (regex, or plain substring — both are tried) and, unless `wait_ready=false`, `document.readyState` is `complete`; then returns final `url`, `title` and `ready_state`. Use after a click or `open_url` that navigates; `wait_for(url_pattern=...)` only checks the URL and can return while the new document is still blank. Uses the same bounded synchronous checks and receipt recovery as `wait_for`. A pending probe with `reservation_held=false` no longer blocks the tab; its delayed reply remains available through `get_execute_js_result` in the same MCP session. - `url_pattern` (string): regex or substring to match against the URL, `timeout` (number, optional): default 15, `wait_ready` (boolean, optional): require `readyState === 'complete'`, default `true`, `session_id` (string, optional) - **scroll_page** — scroll and report the new position, so a long page can be read in passes. - `to` (string, optional): default `bottom`; also accepts `top`, a pixel offset, or a CSS selector to bring into view, `session_id` (string, optional), `timeout` (number, optional): default `15` - **execute_js** — run JavaScript in the page and return the result. `timeout` is one end-to-end deadline covering dialog-policy setup, monitor snapshots, delivery/retry, navigation inspection, and cleanup; an explicit `session_id` is forwarded through every one of those roundtrips instead of relying on the process default. Set `wait=false` for a genuinely long task: once the extension acknowledges delivery, BTAP returns `status="in_progress"` plus an `operation_id`; claim the result with `get_execute_js_result` instead of replaying the script. `dialog_policy="manual"` is intentionally unavailable in background mode. When a script navigates the page, `status` is `navigated` (not `success`) with `landed_url`; the script's return value is genuinely lost in that case and is reported as such rather than substituted. `dialog_policy` decides what happens if the script opens `alert`/`confirm`/`prompt`: `dismiss` (default) and `accept` answer it and report it under `dialogs`, while `manual` pauses a synchronous script with the native dialog still open and returns `blocked_by_dialog` — call `handle_dialog` to release it. A tab already holding a manual pause returns `busy` immediately. Use `wait_for`/`wait_for_url` instead of delayed `setTimeout` or sleep Promises when waiting for page state. When the JSON-encoded `js_return` exceeds the 24 KiB UTF-8 inline limit, BTAP writes the complete value to a private temporary JSON file and returns `result_file`, `result_bytes`, `result_sha256`, and `result_format` instead of a truncated inline value. - A `Cannot access contents of the page` error must be classified before retrying: if the script attempted `window.open` or navigation, use `open_new_tab` (Chrome may block it without a user gesture); if injection into the current tab is forbidden, choose a normal scriptable `http/https` tab or the supported CDP route. Do not treat the message as proof that reading the current page failed. - `timeout` does not cancel dispatched JavaScript. See [Troubleshooting](https://github.com/LinVireo/browsertap-mcp/blob/main/docs/TROUBLESHOOTING.md) for timeout recovery and frame-preparation errors. - `script` (string), `session_id` (string, optional), `no_monitor` (boolean, optional): default `false`, `timeout` (number, optional): default `15`, `dialog_policy` (string, optional): `dismiss` (default), `accept`, or `manual`, `wait` (boolean, optional): default `true` - All routes use the same result conversion: `undefined` and non-finite numbers become `null`; BigInt/symbol become strings; DOM, Error and function values become readable representations. Cycles and depth 6 have markers; iterables keep up to 200 items plus a truncation marker. A `result_file` preserves the complete **converted** value, including those markers. - Once user code starts, a script error does not trigger a second execution. Use an explicit `return` in a complex async body, preferably `(async () => { /* work */ return value; })()`. Ambiguous bodies may complete with `null`; `await(expr)` can parse as a call to an ordinary function named `await`, so use the async IIFE when that distinction matters. - Unpaired UTF-16 values or keys use the same file export at any size, with `result_file_scope="js-value"`. If writing fails, `result_json` preserves the complete value with `result_json_scope="js-value"`; see [Complete JSON results](#complete-json-results) for decoding and receipt semantics. - **get_execute_js_result** — read or briefly wait for an `operation_id`, from the same MCP session that submitted it. Accepts handles from `execute_js` and other timed-out bridge commands. Querying never replays the operation. A completed result can be read repeatedly, including after a lost query response; pending, unknown/expired, and foreign handles return explicit statuses or errors. After reservation expiry, the first valid late terminal reply is retained as `late_result` (`success` and `data`), with `late_reply_age` in seconds. The original `unknown` receipt and `retry_safe=false` remain; the late reply neither restores the reservation nor renews retention. Results are retained for up to 10 minutes, with at most 512 completed operation records; capacity pressure can evict them earlier. A missing result does not prove the operation was never executed. Large successful values use the same lossless `result_file` metadata as `execute_js`; for a late value, that metadata is inside `late_result` and its `data` becomes null. Accepts the nested `reconciliation.bridge_operation.operation_id` from a failed `open_new_tab` status probe. A known read-only wait, inventory or creation-status probe may release its reservation on timeout while retaining its receipt. Check `reservation_held`; it describes that probe, not the outcome of the tab creation. - `operation_id` (string), `timeout` (number, optional): default `0`, range `0`–`120` - **handle_dialog** — inspect or answer a dialog left open on a tab. `action="manual"` reports it without choosing (`blocked_by_dialog`, or `no_dialog` if nothing is open); `accept`/`dismiss` answer it and release any paused `execute_js` or `open_url`. `prompt_text` supplies the text for an accepted `prompt`. - `action` (string), `prompt_text` (string, optional), `session_id` (string, optional), `timeout` (number, optional): default `3`, capped at three seconds - **resolve_leave_dialog** — for an already-open shell/ttyd/IDE leave prompt: two protocol accepts, then physical Enter only when lab permits it. - `session_id` (string, optional) - **upload_files** — set files on a file input, which JavaScript cannot do (`input.files` is read-only). Runs as one CDP batch so the DOM node ids stay valid across the sequence. - `selector` (string): the ``, `paths` (string or array of strings): absolute local paths, `session_id` (string, optional), `timeout` (number, optional): default `30` - **get_cookies** — read cookies for a page. - `session_id` (string, optional), `tab_id` (integer, optional) - **set_cookies** — write cookies into the real browser profile. Takes one cookie object or a list (JSON text is accepted): `name` is required, plus optional `value`/`url`/`domain`/`path`/`expires` (Unix seconds)/`httpOnly`/`secure`/`sameSite`. Uses CDP `Network.setCookie`, so HttpOnly and cross-path cookies work; falls back to `document.cookie` only when CDP is unavailable, and then reports which cookies could not carry HttpOnly. Cookies with neither `url` nor `domain` are scoped to the current page. - `cookies` (string or list or dict), `session_id` (string, optional), `tab_id` (integer, optional), `timeout` (number, optional): default `20` - **delete_cookies** — delete a cookie by name. Uses CDP `Network.deleteCookies`, falling back to expiring it via `document.cookie`. Scope with `domain`/`path`, or `url` to target one site. - `name` (string), `domain` (string, optional), `path` (string, optional), `url` (string, optional), `session_id` (string, optional), `tab_id` (integer, optional), `timeout` (number, optional): default `20` - **storage_get** — read localStorage or sessionStorage. Omit `key` to page with `offset`/`max_items`/`max_bytes`; returns `next_offset` and `truncated`. The default timeout is 30s and a failed call does not close the MCP session. - `key` (string, optional), `area` (string, optional): `local` (default) or `session`, `session_id` (string, optional), `timeout` (number, optional): default `30`, `offset` (integer, optional), `max_items` (integer, optional), `max_bytes` (integer, optional) - **storage_set** — write one localStorage/sessionStorage value (non-string values are JSON-encoded first). Verifies by read-back, so a quota-full or privacy-mode failure is reported instead of silently lost. - `key` (string), `value` (string), `area` (string, optional): `local` (default) or `session`, `session_id` (string, optional), `timeout` (number, optional): default `30`
Background page input Role locators exclude hidden, `aria-hidden`, and inert controls, including inactive forms retained by SPAs. A visible disabled control can satisfy a query; CSS queries retain DOM-presence semantics. Multiple visible role matches still return `ambiguous`. `page_click`, `page_type`, and `wait_for` accept nested same-origin and cross-origin iframe paths, including frames in a separate renderer (OOPIF): ```python control = {"frame": ["#outer", "#inner"], "css": "#control"} wait_for(selector=control, session_id=session_id) page_type("example", selector=control, clear=True, session_id=session_id) page_click(selector={"frame": ["#outer", "#inner"], "role": "button", "name": "Search"}, session_id=session_id) ``` Each call binds the actual frame documents and elements. Navigation or replacement after binding returns `stale_frame`; a later, independent call can locate the new document. Check `input_dispatched` before recovery: a partial sequence or unknown result must not be replayed. An iframe wait with an outstanding `operation_id` can retain its reservation; collect that result first. Old extension builds return `stale_extension` with `next_action=reload_extension`. Trusted CDP input events delivered to one named tab. They do **not** activate the tab, focus its window, or move the desktop cursor — every reply carries `foreground_changed: false` and `input_mode: "cdp"`. All coordinates are **viewport CSS pixels** (relative to the top-left of the page area, the space `getBoundingClientRect` reports), never desktop pixels and never the device pixels `capture_page_screenshot` returns. Pass `session_id` explicitly: the call holds that target in its own context without changing this MCP process's default. A stale handle without evidence of a same-tab replacement is refused. Another MCP call using the same tab can return `target_busy`; see the concurrent-task boundaries above. `selector` remains backward-compatible with CSS strings and also accepts a locator object with exactly one primary key: `css`, `role` (optional `name`), `text`, or `label`. Inside a locator object, `selector` is a compatibility alias for `css`. `exact` applies to role/name or text matching; `frame` walks iframe locators across origin and renderer boundaries; `shadow` walks open Shadow DOM hosts. A click-only frame-relative point can use `{"frame": [...], "x": 20, "y": 30}`; its CSS coordinates in the final frame's viewport are converted to top-document coordinates before dispatch. Zero matches return `not_found`, multiple matches return `ambiguous`, and closed shadow roots remain inaccessible. Framed clicks refuse non-identity transforms, zoom or perspective on the frame or its ancestors with `unsupported_frame_transform`; query/type paths remain available. - **page_click** — click a CSS/structured `selector` or viewport coordinates. Exactly one targeting mode: either `selector`, or both `x` and `y`. With a selector, each omitted offset axis uses the element centre; a supplied `offset_x` or `offset_y` is measured from the element's top-left corner. `{"frame": [...], "x": 20, "y": 30}` names a point in the final iframe, converted to top-document CSS coordinates. Point mode is not hit-tested. Missing, ambiguous, non-interactable, closed-shadow, and unsupported transformed-frame targets dispatch no input. Selector mode checks the element and every parent frame for obstruction before dispatch: an overlay returns `obscured` with `occluded_by`, and an off-screen point returns `outside_viewport`. Top-document targets can be scrolled into view (`scrolled_into_view`); framed clicks do not scroll automatically. A verified click carries `hit_verified: true`. Screenshot coordinates are device pixels: divide by `devicePixelRatio` before using them as CSS coordinates. Challenge replies keep the bounded `challenge_detected`/`attempts`/`challenge_stalled` behavior. - `selector` (string/object, optional), `x` (number, optional), `y` (number, optional), `offset_x` (number, optional), `offset_y` (number, optional), `button` (string, optional): default `left`, `clicks` (integer, optional): default `1`, `session_id` (string, optional), `timeout` (number, optional): default `15` - **page_type** — insert text into a CSS/structured-locator field, or into whatever already has focus when `selector` is omitted. Xterm.js containers/descendants retarget to `.xterm-helper-textarea`. Missing, ambiguous, read-only, or otherwise unusable targets return a structured status without dispatching text or keys; invalid legacy CSS returns `status="invalid_selector"` instead of a raw `SyntaxError`. Successful and failed target resolution includes a redacted `active_element` descriptor and `focus_confirmed` when focus was attempted, so omitted-selector input is auditable. `clear=true` selects the existing value first; `submit_key` sends one key afterwards. - `text` (string), `selector` (string/object, optional), `clear` (boolean, optional): default `false`, `submit_key` (string, optional), `session_id` (string, optional), `timeout` (number, optional): default `15` - **page_press** — press a key or a comma-separated modifier chord in the tab, e.g. `enter` or `ctrl,shift,k`. - `keys_csv` (string), `session_id` (string, optional), `timeout` (number, optional): default `15` - **page_drag** — drag between two viewport points as one uninterrupted event sequence. - `x1` (number), `y1` (number), `x2` (number), `y2` (number), `duration` (number, optional): default `0.3`, `button` (string, optional): default `left`, `session_id` (string, optional), `timeout` (number, optional): default `15`
Site permissions Temporary, origin-scoped permission leases backed by `chrome.contentSettings`. Every lease records the prior setting and attempts restoration on expiry, explicit reset, and a service-worker or browser restart. Unsupported restoration retains `manual_recovery` with the prior setting and stops automatic retries. - **set_site_permission** — set one permission for one origin, for 60–600 seconds. Supported: `notifications`, `geolocation` (or `location`), `camera`, `microphone`. `setting` is `allow`, `block`, or `ask`. In `safe`, every `allow` requires approval; default `lab` applies it without elicitation (`BROWSERTAP_LAB_NO_ELICIT=1` semantics). Declining returns `requires_user_action` and changes nothing. `clipboard` returns `unsupported`, because its exact prior state cannot be restored. Omit `origin` to use the target tab's current origin; only `http`/`https` origins are accepted. If an established lease loses restoration support, `manual_recovery` retains its prior setting and recovery guidance and stops automatic retries. - `permission` (string), `setting` (string): `allow`, `block`, or `ask`, `origin` (string, optional): defaults to the tab's origin, `duration_seconds` (integer, optional): 60–600, default `300`, `session_id` (string, optional) - **reset_site_permissions** — attempt to restore matching leases now, including `manual_recovery` records. Omit both `origin` and `permission` to reset every lease on that browser. Unsupported restoration preserves the prior setting and recovery guidance and stops automatic retries; resolve the cause before another explicit reset. - `origin` (string, optional), `permission` (string, optional), `session_id` (string, optional)
CDP - **cdp_command** — send one CDP command to the selected tab or explicit debuggee. Listed high-risk methods return `raw_cdp_blocked` before dispatch; params must be a JSON object. Other allowed methods can still change page or profile state. See the raw CDP policy above. - `method` (string): e.g. `Page.navigate`, `params_json` (string, optional): JSON object as text, `session_id` (string, optional), `tab_id` (integer/string, optional), `extension_id` (string, optional), `target_id` (string, optional), `timeout` (number, optional): default `20` - **cdp_batch** — send a batch; `batch_json` must be a JSON object with `cmd: "batch"` and a `commands` array of `cdp`, `tabs`, or `cookies` objects. The whole batch passes the raw CDP policy before its first member runs; nested/unknown commands are rejected. - `batch_json` (string), `session_id` (string, optional) - **debugger_targets** — *(no tab needed)* list every CDP-attachable target, including service workers and extension background pages that `list_tabs` never shows. - `session_id` (string, optional) - **save_pdf** — bounded `Page.printToPDF`; validates PDF bytes and atomically writes `save_path`. `save_path` is **relative** and resolves under `~/Downloads/browsertap`; an absolute path or a `..` escape is rejected with `ValueError`. A timeout forcibly releases its debugger lease. - `save_path` (string), `session_id` (string, optional), `landscape` (boolean, optional): default `false`, `print_background` (boolean, optional): default `true`, `prefer_css_page_size` (boolean, optional): default `true`, `scale` (number, optional): default `1.0`, range `0.1`–`2.0`, `page_ranges` (string, optional), `timeout` (number, optional): default `30` > **On driving *other* extensions:** Chrome refuses cross-extension debugging at attach time, and all three addressing forms (`tab_id`, `extension_id`, `target_id`) are rejected alike unless Chrome was started with `--silent-debugger-extension-api`. These parameters are for this extension's own targets and for diagnosis.
Extension management - **extension_path** — absolute path of the unpacked extension, for manual install. No parameters. - **list_extensions** — *(no tab needed)* installed extensions with id, name, enabled state, type, and version. - `session_id` (string, optional) - **set_extension_enabled** — *(no tab needed)* enable or disable an installed extension. Chrome exposes no API to *install* one, so this only toggles what is already there. - `extension_id` (string), `enabled` (boolean), `session_id` (string, optional) - **uninstall_extension** — *(no tab needed)* request removal of another extension. Confirmation defaults on; set it off only for an explicitly selected disposable/test extension. Chrome can refuse either setting because a user gesture is required; changing the flag does not supply that gesture. BTAP cannot uninstall itself through its active response channel. - `extension_id` (string), `show_confirm_dialog` (boolean, optional): default `true`, `session_id` (string, optional) - **get_bookmarks** — *(no tab needed)* read the bookmark tree. - `session_id` (string, optional) - **create_bookmark** — *(no tab needed)* create a bookmark or folder. - `title` (string), `url` (string, optional): omit to create a folder, `parent_id` (string, optional), `session_id` (string, optional) - **remove_bookmark** — *(no tab needed)* atomically save the target subtree under `bookmark-backups` in the local state directory, then remove the bookmark or folder. Returns `backup_path` and `backup_sha256`; backup failure prevents deletion. The managed backup subdirectory must be an ordinary directory, not a symlink or reparse point. Limits: 16 MiB per file, 100 files and 64 MiB total; backups older than 30 days or over capacity are removed on the next backup. A lost deletion receipt keeps the backup evidence but requires inspecting the bookmark tree before retrying. - `bookmark_id` (string), `recursive` (boolean, optional): default `false`, `session_id` (string, optional) - **call_extension** — *(no tab needed)* send JSON to another enabled extension; the target must allow BTAP via `externally_connectable`. - `extension_id` (string), `message_json` (string): JSON payload as text, `session_id` (string, optional)
Network and console capture - **network_capture_start** — start collecting bounded request/response records and optional bodies. Defaults: 500-entry ring and 256 KiB per body. - `session_id` (string, optional), `include_bodies` (boolean, optional): default `true`, `max_entries` (integer, optional): default `500`, range 10–2000, `max_body_bytes` (integer, optional): default `262144`, range 1024–2097152, `body_timeout` (number, optional): default `5`, range 0.1–10 seconds, `timeout` (number, optional): default `10` - **network_capture_stop** — return the current capture and release its debugger lease; always call it in cleanup. Returned records can be filtered without changing capture bounds or cleanup. `url_pattern` is compiled by the browser as a JavaScript `RegExp`; invalid patterns return a structured error and leave the capture running for retry. - `session_id` (string, optional), `url_pattern` (string, optional): JavaScript `RegExp`, `resource_type` (string, optional), `status_min`/`status_max` (integer, optional): 100–599, `include_response_bodies` (boolean, optional): default `true`, `timeout` (number, optional): default `10` - **console_capture_start** — start collecting `console.*` and uncaught exceptions. - `session_id` (string, optional), `max_entries` (integer, optional): default `500`, range 10–5000, `timeout` (number, optional): default `10` - **get_console_messages** — page through or clear the current console buffer. `filter='user'` retains page MAIN/default-context output and excludes isolated extension/content-script contexts; empty/`all` preserves the complete buffer. - `session_id` (string, optional), `offset` (integer, optional): default `0`, `max_items` (integer, optional): default `200`, `clear` (boolean, optional): default `false`, `filter` (string, optional): `user` or `all`, `timeout` (number, optional): default `10` - **console_capture_stop** — return the remaining console messages and release its debugger lease. - `session_id` (string, optional), `timeout` (number, optional): default `10`
Screenshots - **capture_page_screenshot** — page capture via CDP with viewport, `full_page`, or explicit `clip` modes. PNG, JPEG, and WebP are supported; `quality` is valid only for JPEG/WebP. Returns text metadata plus attached MCP image content; `save_path` only adds a disk copy, and it is **relative** — it resolves under `~/Downloads/browsertap`, with absolute paths and `..` escapes rejected. Base64 is omitted unless explicitly requested. The metadata names its own units: `image_width`/`image_height` parsed from the returned bytes and `pixel_space: "device"` (CSS × `devicePixelRatio`), so a point read off the picture is not fed straight to `page_click`. A header it cannot parse reports `null` dimensions plus a `dimensions_note` rather than a guess — `size` is the byte count, not a dimension. - `session_id` (string, optional), `tab_id` (integer, optional), `format` (string, optional): default `png`, `full_page` (boolean, optional): default `false`, `clip` (object, optional): `x`,`y`,`width`,`height`, optional `scale`, `quality` (integer, optional): 0–100 for JPEG/WebP, `save_path` (string, optional), `return_base64` (boolean, optional): default `false`, `timeout` (number, optional): default `20`
Native file dialogs: explicit desktop opt-in - **inspect_native_file_dialog** — inspect the current foreground Windows standard Shell file dialog owned by a registered Chrome or Edge process. Requires `[desktop]`. Checks owner/process identity, native controls, visibility and Cancel hit targets, then installs a temporary lifetime marker and returns a 15-second ticket. This has a temporary marker side effect and does not activate a window. Unsupported platforms, portable/unregistered browsers, cross-process owners and unrecognized layouts are refused. - `desktop_opt_in` (boolean, optional): default `false`; must be `true` to inspect. - **cancel_native_file_dialog** — consume a fresh inspection ticket and send one bounded message to that dialog's Cancel button. Requires `[desktop]` and the current safe/lab physical-approval policy. Rechecks the cross-process lease, observed Windows quiet-input state, held keys/buttons, identity, foreground and hit targets. Returns `status="success", cancelled=true` only after observing the original window gone. Uncertain delivery or closure returns `unknown`, `retry_safe=false`; inspect state before another action. Each opted-in attempt consumes the ticket, including refusals. Both tools report `desktop`, `on_screen` and `input_quiet` diagnostics. - `ticket` (string, required), `desktop_opt_in` (boolean, optional): default `false`; must be `true` to cancel. For uploads, use `upload_files` with the page's file input. Native cancellation is an explicit recovery attempt for an already-open supported dialog. Live verification did not obtain a valid ticket for a real Chrome dialog with a cross-process owner, so successful automatic cancellation remains unverified. Do not open a chooser solely to test recovery. Closing its tab may leave the dialog open, and a user's manual closure is not an automated success. `safe` requires approval; default `lab` skips elicitation but still requires opt-in. Tickets expire, are limited to eight per MCP process, and clean up their own markers on consumption, expiry, eviction or orderly shutdown. These checks are not an atomic desktop transaction. See [native-dialog design](https://github.com/LinVireo/browsertap-mcp/blob/main/docs/agent-guides/native-dialogs.md).
Removed in 0.5.0: OS-level input and desktop capture `mouse_move`, `mouse_click`, `mouse_drag`, `type_text`, `hotkey`, `pointer_info` and `capture_desktop_screenshot` no longer exist. They drove the whole desktop rather than one tab, so they acted on whatever happened to be on screen. What to call instead: | Removed | Use | | --- | --- | | `mouse_click` | `page_click` | | `mouse_move` | not needed — `page_click` positions itself | | `mouse_drag` | `page_drag` | | `type_text` | `page_type` | | `hotkey` | `page_press` | | `pointer_info` | `execute_js` to read element geometry | | `capture_desktop_screenshot` | `capture_page_screenshot` | A failing `page_*` call is a targeting problem, not a reason to look for a screen-coordinate fallback — re-read the page with `scan_page` and fix the locator. Browser chrome, extension popups and OS dialogs are outside page-level input. The explicit native-file-dialog tools above cover only the documented Windows inspection/cancellation boundary. One global-key fallback survives: `resolve_leave_dialog` may send Enter after its two protocol attempts fail, in `lab` only; pure probe timeouts do not trigger it. `safe` returns `requires_user_action` without sending Enter. Lab skips elicitation by default; when lab approval prompts are enabled, a declined, cancelled or unavailable prompt also prevents input. The physical gate keeps its cross-process lock (contended → `busy`, returned immediately, never queued), a short quiet window (you touched the mouse or keyboard → `input_activity_detected`, nothing sent), then raise the target tab, then act. What that window can detect depends on the OS: only Windows exposes a last-input timestamp, and the pointer position is unavailable under Wayland, in a headless container, and on macOS without the accessibility permission. With no signal at all the window still elapses but has nothing to compare, so the result carries an `input_quiet` block naming the markers it sampled, with `enforced: false` when there were none — read a pass on such a machine as unverified rather than as an idle desktop. If the tab cannot be confirmed on screen the result is `activation_failed` and nothing is sent, so a minimised window produces an error rather than an Enter into the wrong place.
## Troubleshooting Run `browsertap doctor` first. For connection, version, dialog, permission, and physical-input recovery procedures, see the dedicated [troubleshooting guide](https://github.com/LinVireo/browsertap-mcp/blob/main/docs/TROUBLESHOOTING.md). ## License MIT — see [LICENSE](https://github.com/LinVireo/browsertap-mcp/blob/main/LICENSE), which ships inside both the wheel and the sdist. Keep it if you fork or redistribute this. BTAP is maintained by `LinVireo`, and the canonical public repository for this distribution is `LinVireo/browsertap-mcp`.