Skip to main content
Glama
Chrome Browser Control

Chrome Browser Control

by vKongv

Chrome Browser Control

npm version Node.js License: MIT

Local Chrome-profile control for stdio MCP hosts.

This project exposes browser-control MCP tools through a Manifest V3 Chrome extension connected to a loopback WebSocket broker. Configure your MCP host to launch the stdio adapter with the same pairing token you enter in the extension.

Repository: https://github.com/vkongv/chrome-browser-control

Prerequisites

  • Node.js 20+

  • Google Chrome

Related MCP server: OpenCode in Chrome

Install and Setup

Preferred path: install the CLI, then run setup.

npm install -g chrome-browser-control
# or, without a global install:
npx -y chrome-browser-control setup

The CLI installs as cbctl (preferred short name) and also as chrome-browser-control.

cbctl setup
cbctl start
cbctl doctor

setup writes ~/.chrome-browser-control/config.env (pairing token + port), copies the unpacked extension to ~/.chrome-browser-control/extension, and prints MCP host snippets. Do not commit that directory.

Agent skill (separate from npm)

The runtime agent skill under skills/chrome-browser-control/ is not shipped inside the npm package. Install it with skills.sh, which links it into every agent host you pick (Claude Code, Codex, Cursor, and others) and records it for updates:

npx skills add vkongv/chrome-browser-control -g -s chrome-browser-control
npx skills update -g   # later: pull the latest published skill

skills update is manual; nothing updates the skill in the background. Remove hand-copied skill folders (for example ~/.claude/skills/chrome-browser-control) before the first add so they do not shadow the tracked install.

CLI commands (cbctl or chrome-browser-control):

Command

Purpose

cbctl setup

Create user config and install the extension copy

cbctl start

Start the shared loopback broker

cbctl stop

Stop the broker

cbctl status

Show broker / config status

cbctl doctor

Local setup checker

cbctl mcp

Stdio MCP adapter (attach-only by default)

cbctl mcp-config

Print host-specific MCP snippets

cbctl broker

Run the broker in the foreground (dev)

From a git checkout (contributors):

git clone https://github.com/vkongv/chrome-browser-control.git
cd chrome-browser-control
npm install
npm run build
node dist/cli/main.js setup

Repo-local npm run broker / npm run mcp remain available for development against TypeScript sources (with optional repo .env.local).

Environment Variables

  • CHROME_BROWSER_CONTROL_TOKEN — Required. High-entropy pairing token shared by the broker, MCP adapter, and extension popup.

  • CHROME_BROWSER_CONTROL_PORT — WebSocket broker port (default 8765).

  • CHROME_BROWSER_CONTROL_HOST — Loopback host for the broker (default 127.0.0.1).

  • CHROME_BROWSER_CONTROL_EXTENSION_ID — Optional. Pins the broker to one installed extension ID.

  • CHROME_BROWSER_CONTROL_AUTOLOAD — Optional. Set to 1 so mcp may spawn a broker if none is reachable (recovery). Prefer cbctl start for normal use.

  • CHROME_BROWSER_CONTROL_DISABLE_LOCAL_ENV — Optional. Set to 1 to skip loading repo .env.local.

User config lives under ~/.chrome-browser-control/ and is loaded before any repo .env.local. Process env always wins.

MCP attach-only default: cbctl mcp connects to an already-running broker. Start the broker with cbctl start first. For recovery, use cbctl mcp --autoload or CHROME_BROWSER_CONTROL_AUTOLOAD=1.

Load The Extension

  1. Open Chrome with the profile you want the MCP tools to control.

  2. Go to chrome://extensions.

  3. Enable Developer mode.

  4. Click "Load unpacked".

  5. Select ~/.chrome-browser-control/extension (printed by setup). Contributors editing sources may load extension/ from the repo instead.

  6. Open the Chrome Browser Control extension popup.

  7. Keep the bridge URL at ws://127.0.0.1:8765 unless you changed the local port.

  8. Paste the generated pairing token.

  9. Add allowed origins such as https://example.com, http://localhost:3000, or * for all normal http:// and https:// pages.

  10. Click "Save and reconnect".

The extension may ask for host permission for the allowed origins. Denying that request prevents page actions for those origins.

Using * is convenient for local development, but it exposes every normal web page in the current Chrome profile to MCP tools. Prefer explicit origins when you only need a few sites. Wildcard mode also asks for the optional <all_urls> host permission so Chrome allows visible-viewport screenshots through chrome.tabs.captureVisibleTab; the background still blocks non-http(s) URLs and disallowed origins before capture.

MCP Host Configuration

Paste a snippet from cbctl setup (or mcp-config) into Cursor, Claude Desktop, Codex, or another stdio MCP host. To print host-specific config again later:

cbctl mcp-config --host cursor
cbctl mcp-config --host claude
cbctl mcp-config --host codex
cbctl mcp-config --host yaml

The MCP server key is chrome_browser_control. The adapter command is the installable CLI (cbctl preferred) with args: ["mcp"] — not tsx against server/index.ts.

YAML-style example:

mcp_servers:
  chrome_browser_control:
    command: "cbctl"
    args: ["mcp"]
    env:
      CHROME_BROWSER_CONTROL_TOKEN: "<generated-token>"
      CHROME_BROWSER_CONTROL_PORT: "8765"
    timeout: 60
    connect_timeout: 30

JSON-style example:

{
  "mcpServers": {
    "chrome_browser_control": {
      "command": "cbctl",
      "args": ["mcp"],
      "env": {
        "CHROME_BROWSER_CONTROL_TOKEN": "<generated-token>",
        "CHROME_BROWSER_CONTROL_PORT": "8765"
      }
    }
  }
}

If the CLI is not on PATH, use the NPX fallback printed by setup: npx with args: ["-y", "chrome-browser-control", "mcp"].

If your MCP host uses a config file, keep it private and outside the repository.

Verify

  1. Start the broker: cbctl start

  2. Run the setup checker: cbctl doctor

  3. Confirm from your MCP host by calling the browser_status tool. When ready, extension.status should reflect a live bridge connection, and extension.allowedOrigins should show your configured scope.

Tools

  • browser_status: checks whether the MCP adapter can reach the broker and whether the Chrome extension answers ping. When ready, extension.status reflects the live bridge connection (not a stale disconnected default), extension.allowedOrigins shows the configured scope (including * (all http/https web origins) when wildcard mode is enabled), extension.session shows session name/claimed tabs, extension.cdpEnabled / extension.attachedTabs report the trusted-input tier, and protocolVersion / features confirm the loaded unpacked extension code. Protocol version 7 includes the cdp-trusted-input and cdp-response-body feature markers.

  • name_session: sets a human-readable session name for status/debugging.

  • list_tabs: lists tabs whose URL origin is allowed in the extension popup. When every open tab is filtered out, returns { tabs: [], detail, hiddenTabCount, allowedOrigins? } instead of a bare []. Wildcard mode is labeled clearly in allowedOrigins.

  • list_frames: lists current frame documents for an allowed tab using Chrome's frame registry. Operable active HTTP(S) documents include a documentId; policy-blocked, host-permission-denied, unsupported, fenced, and non-active rows retain hierarchy/status only and redact URL and document identity.

  • claim_tab: claims an allowed tab for this browser-control session and returns a sessionTabId. Claims are routing state, not exclusive browser locks. With exclusive: true, the claim also holds a lease that other agents cannot take; ttlMs (default 5 minutes) is idle time, extended by every call the claiming MCP session makes with the sessionTabId. Using an expired sessionTabId fails with TAB_CLAIM_EXPIRED.

  • release_tab: releases a claim by sessionTabId or tabId without closing the tab. Releasing an unclaimed tab returns { released: false, reason: "not_claimed" }.

  • finalize_tabs: releases claim state for the session without closing tabs. Pass keep entries to preserve handoff/deliverable claims. Also detaches CDP attachments on every claimed tab, including kept claims.

  • cdp_attach: attaches trusted input to a claimed tab (sessionTabId required). Off by default until the popup enableCdp checkbox is on. The debugger permission is required in the manifest; Chrome shows a warning at load. Chrome shows a persistent debugging banner on the attached tab. While attached, click, type, keypress, click_at, and matching perform_actions steps use CDP. Calling cdp_attach again on the same tab refreshes the TTL without tearing down the live socket. Cross-origin iframe clicks fail with CDP_CROSS_ORIGIN_FRAME; detach or use a same-origin/top document.

  • cdp_detach: detaches trusted input and returns those tools to the content-script path without releasing the claim.

  • cdp_network_watch: enables Network on an attached tab and keeps an in-memory metadata index (requestId, url, method, status, mimeType, size, timestamp). No bodies are stored. Restricted-category origins never enter the index.

  • cdp_network_requests: returns that index.

  • cdp_response_body: reads one body by requestId after an allowlist and denylist check. Binary bodies are refused. Bodies over the size cap return an error, never a truncated body. Token-shaped fields receive best-effort masking, which is not a guarantee. Treat every body as credential-bearing. Response headers, Set-Cookie, and request post data are never returned.

  • snapshot: returns a simplified DOM snapshot for an allowed document. By default this is a compact automation snapshot that includes concise actionable elements, a text preview (500 chars), omitted counts, and region summaries. Compact defaults to main-landmark scope when present; a visible modal dialog (aria-modal="true" or <dialog> opened with showModal()) takes that scope instead. Other visible role="dialog" nodes are included in the current scope and do not steal it. Hidden or closed dialogs have no effect. Pass scope: "document" for the full body (including the page behind a modal), ignoreRoles: ["dialog"] or ["alertdialog"] to hide both dialog and alertdialog, ignoreRoles: [] to include them in the current scope, or mode: "full" for the unscoped legacy snapshot. Pass mode: "visible" for viewport/intersection-aware elements with bounds and scroll metadata. Pass textLimit (up to 100000) when you need more page body text — check textBytesOmitted to see if content was truncated. Snapshot text keeps block structure (# headings, - list items, Markdown table rows, fenced pre blocks), so compact mode with a raised textLimit is the way to read a page; mode: "full" adds element bounds for layout work. Interactive elements from the elements list also appear in the text where they sit on the page, with the same refs: links as [API docs](ref=h3), other controls as [button "Save" ref=h4] or [button ref=h5] when unlabeled. Inline refs cover only the text returned under textLimit; the elements list is still the complete set. Controls that hold content, such as editable regions or card links, keep that content with the marker in front, and form fields show their label but never their value. The list leaves out decorative aria-hidden icons and images (ariaHiddenOmitted counts them), and icon-only controls are named from their image alt text. When the page declares a Markdown version (<link rel="alternate" type="text/markdown">) or visibly links one ("View as Markdown"), the snapshot includes markdownAlternate with its URL and source (link when declared, anchor when inferred from a link).

  • visible_snapshot: convenience tool for snapshot({ mode: "visible" }).

  • navigate: navigates the active tab or a specified tabId to an allowed URL, then waits for the tab to finish loading when possible. By default focus is unchanged (background tabs stay in the background; the focused tab is not deactivated). Pass active: true only when the tab must become visible. If loading times out, the result includes pending: true and a warning. Supports after observations after the load wait.

  • activate_tab: focuses an allowed tab and its Chrome window without changing the URL. It waits until the document reports visibilityState: "visible" or a short timeout elapses, then returns visibilityState and visible along with active and focused. When visible is false, the result also includes reason (hidden, unknown, host_permission_denied, or document_unavailable). focused is not a success signal — a window can stay unfocused on macOS while the document still becomes visible. Use this when click/type/keypress fail with DOCUMENT_HIDDEN, or when the tab is already active in an unfocused window. Pass allowHidden: true only when visibilityState is hidden. If reason is host_permission_denied or document_unavailable, grant host permission or reload the tab; allowHidden will not help. screenshot.activated only reports whether the tab was made active in its window — not whether the window was focused or the document became visible.

  • click: clicks an element by snapshot ref on an allowed tab. Fails with DOCUMENT_HIDDEN when the document is hidden unless allowHidden: true. Supports after observations.

  • type: types into an element by snapshot ref on an allowed tab. Password-like fields are blocked unless force=true. Fails with DOCUMENT_HIDDEN when the document is hidden unless allowHidden: true. Supports after observations.

  • scroll: scrolls an allowed tab by deltaX and deltaY. Optional x/y viewport coordinates scroll a scrollable element under that point when one is found. Scrolling does not paginate snapshot text — snapshots use full document.body innerText. Raise textLimit on snapshot instead of scroll-stitching unless the page lazy-loads content. Supports after observations.

  • query_elements: returns bounded refs/roles/labels/bounds for elements filtered by CSS selector, role, text, and visibility.

  • extract_elements: extracts bounded text/html/links/time data from a CSS selector. HTML extraction redacts password/OTP/hidden-token attribute values and marks sensitive items instead of leaking secret values. This is the supported alternative to raw JavaScript evaluation.

  • screenshot: captures the visible viewport of an allowed tab as a data URL. Optional ref or bounds (+ padding) crop after capture; empty crops fail before captureVisibleTab. Uncropped responses omit crop fields. MV3 capture is viewport-only; inactive target tabs may be activated before capture. Chrome requires <all_urls> or activeTab for captureVisibleTab; this extension requests optional <all_urls> only in wildcard (*) mode, so wildcard screenshots need that popup grant.

  • keypress: dispatches common DOM keyboard events to the page. Browser/OS-level shortcuts are not guaranteed under MV3. Fails with DOCUMENT_HIDDEN when the document is hidden unless allowHidden: true. Supports after observations.

  • click_at: dispatches mouse events at viewport coordinates. Fails with DOCUMENT_HIDDEN when the document is hidden unless allowHidden: true. Supports after observations.

  • wait_for: waits for bounded selector/text/URL-substring, selector-absence, scoped-text, content-stability, or settled-change conditions and returns match/timeout evidence. Conditions are alternatives; the first that holds wins. settledMs waits until scoped text changes, then holds still that long with nothing in scope loading (aria-busy="true", an indeterminate progressbar, or a "Loading…" line); contentStableMs also waits while scope is loading. A timeout reports pending (noChange, busy, changing) and busy.

  • page_status: returns title, URL, ready/visibility state, viewport/scroll state, and resource counts by initiator type. It does not expose request headers or response bodies.

  • console_logs: returns bounded console logs captured after the content script was injected. It cannot see older page console history.

  • collect_scroll: scrolls a bounded number of steps (hard ceiling when until is set), extracts selected elements each step, optionally targets a nested scroll container via scroll, applies an aggregate item cap (maxItems, default 100), and optionally dedupes by text or href for lazy feeds. Optional until.noNewItemsForSteps / until.stopBeforeDatetime (ISO-8601; requires includeTimes) set stoppedReason. Results include omitted/truncated counts. Supports after observations.

  • perform_actions: runs up to 10 sequential page actions (click, type, scroll, keypress) in one broker round-trip. Fail-fast on the first step error; terminal after observations run only when every step succeeds. Click, type, and keypress steps fail with DOCUMENT_HIDDEN on hidden documents unless that step sets allowHidden: true. Scroll steps stay unguarded. Coordinate clicks stay on single-tool click_at. Steps cannot carry after, tabId, or sessionTabId.

Frame document targeting

DOM/content tools accept an optional documentId returned by list_frames. Omitting it preserves existing behavior and targets the current top document for each operation. Supplying it selects that exact document: if the iframe navigates, disappears, moves to another tab, becomes unsupported, or loses access, the operation fails instead of falling back to the top frame or a replacement using the same frameId.

Every content result carries background-attested documentId, frameId, isTopFrame, and coordinateSpace. Top-frame coordinates use tabViewport; iframe visible_snapshot bounds, click_at, and coordinate scrolling use frameViewport. Iframe-local bounds cannot be passed to screenshot cropping because screenshot remains a tab-viewport-only tool. navigate, activate_tab, screenshot, and list_frames accept tab targets only; perform_actions.documentId applies to the whole batch and cannot be overridden by a step.

Document failures preserve one of these prefixes, including inside batch step errors and after failures: DOCUMENT_STALE:, DOCUMENT_POLICY_DENIED:, DOCUMENT_HOST_PERMISSION_DENIED:, or DOCUMENT_UNSUPPORTED:. V1 supports only active HTTP(S) outermost/subframe documents. It intentionally excludes about:blank, about:srcdoc, blob:, data:, origin-fallback frames, iframe navigation, and iframe-to-tab screenshot coordinate translation.

Act Then Observe

The action tools navigate, click, type, scroll, keypress, click_at, collect_scroll, and perform_actions accept an optional after object. The extension removes after before sending the base action to the content script, then runs requested observations in this fixed order: waitFor, snapshot, pageStatus. The response is the base action result plus an after object with the observation results.

For perform_actions, after applies to the whole batch only: individual steps cannot include after, and terminal observations are skipped when any step fails. Partial batch failures return structured step results with failedIndex and completedCount while preserving bridge-level success so agents can inspect the payload.

{
  "ref": "h12",
  "after": {
    "waitFor": { "settledMs": 750, "timeoutMs": 8000 },
    "snapshot": { "mode": "visible", "limit": 40 },
    "pageStatus": true
  }
}

after.waitFor must include at least one wait condition. To wait for the result of the action, use settledMs: the extension records the scoped text before the action runs, and the wait returns once that text has changed and held still for settledMs with nothing in scope loading. After navigate, the loaded page counts as changed. For text, selector, selectorAbsent, textInScope, and urlIncludes, the extension also checks the condition before the action and returns heldBeforeAction; true means the match may be something that was already on the page. timeoutMs is optional and capped at 20000 so the full act-then-observe chain stays within the default broker request timeout. after.snapshot may be true for default snapshot options or an object with mode, textLimit, and/or limit. Invalid after requests are rejected before the base action runs.

If the base action succeeds but an after observation fails, the response still includes the base action result and sets after to { "ok": false, "error": "..." }.

Snapshot Modes And Refs

Default compact snapshots are designed to reduce model-context usage while preserving browser automation. A compact snapshot looks like:

{
  "title": "Example Domain",
  "url": "https://example.com/",
  "mode": "compact",
  "elements": [{ "ref": "h1", "role": "link", "label": "Learn more" }],
  "omittedElements": 0,
  "textPreview": "Example Domain ...",
  "textBytesOmitted": 0,
  "regions": []
}

Use full mode only when you need the legacy verbose element metadata:

{ "mode": "full", "tabId": 123 }

Use visible mode for viewport-bound work, virtualized pages, and click-coordinate planning:

{ "mode": "visible", "sessionTabId": "tab-1" }

To read long page content (for example API docs), raise textLimit instead of using broker scripts or CDP workarounds:

{ "mode": "full", "textLimit": 100000, "tabId": 123 }

Compact mode honors textLimit too; body text is returned in textPreview (there is no text field in compact mode). When textBytesOmitted is greater than zero, increase textLimit or scroll the page and snapshot again only if content is lazy-loaded below the fold.

When a visible genuinely-modal dialog is open (aria-modal="true", or a <dialog> shown with showModal() — the open attribute alone is not enough), compact snapshots set scopeApplied to dialog and read that dialog instead of the page behind it. role="alertdialog" uses the same modal predicate; without aria-modal or :modal it does not take scope. A top-document snapshot does not see a modal inside an iframe — pass that frame's documentId. The same scope, ignoreRoles, and mode escape hatches listed on snapshot still apply.

Refs are per-document in-memory IDs (h...) assigned from element identity, not output order. They remain stable across DOM insertion/reorder in the same document, and click / type resolve through the content script's ref store. Refs can collide between frame documents, so retain the result's documentId and pass it with later iframe actions. Navigating to a different page loads a new document, so old refs are expected to fail cleanly; take a fresh snapshot after navigation or major page changes. The ref store prunes disconnected, expired, and over-cap entries, and removes stale data-cbc-ref attributes so pruned refs cannot be reused accidentally.

Tab Sessions

Prefer claim_tab before multi-step browser work:

{ "tabId": 123 }

The returned sessionTabId can be passed to snapshot, navigate, activate_tab, click, type, scroll, query_elements, extract_elements, screenshot, wait_for, and related page tools. If a session has a current claim, page actions without an explicit tabId or sessionTabId route to that claim. If no claim exists, legacy active-tab fallback remains.

Claims are advisory MCP routing state only. They do not stop the user from changing, closing, or navigating a tab. Use release_tab or finalize_tabs when a task is complete; neither tool closes browser tabs.

Development Checks

npm test
npm run build
cbctl doctor
# or: node dist/cli/main.js doctor
npm run benchmark:compact-snapshots
npm audit

npm run benchmark:snapshots is an alias for the same compact-vs-full benchmark. The benchmark prints compact bytes, full bytes, and reduction percentage; compact mode should stay at least 50% smaller on the dense fixture.

After editing files under extension/, run npm run build, run cbctl setup, then reload the unpacked extension on chrome://extensions before running browser e2e checks. After adapter/server changes, rebuild and restart the MCP host too. A stale loaded background service worker or tool catalog can keep serving older behavior; browser_status should report adapter.registeredToolCount: 30, extension protocol version 7, and the cdp-trusted-input and cdp-response-body feature markers when both sides are current.

Limitations

  • This is a prototype with a shared local token, not multi-user authentication.

  • Browser tool calls are serialized globally at the broker.

  • Content scripts use DOM snapshots, not the full Chrome accessibility tree.

  • Refs are document-scoped in-memory handles. Run snapshot again after navigation, reloads, major DOM changes, or stale-ref errors.

  • Visible screenshots are viewport-only. Capturing an inactive tab may activate it because Chrome MV3 captures the visible tab in a window.

  • Chrome screenshot capture requires <all_urls> or activeTab. This project requests optional <all_urls> as a host permission only for wildcard screenshots. If screenshot reports that this permission is missing, reload the extension after manifest updates, open the popup, save settings, and grant the prompt.

  • keypress and click_at use DOM events unless the tab is CDP-attached. After cdp_attach, they use trusted CDP input (isTrusted: true). They still do not guarantee privileged browser or OS shortcuts.

  • Console logs are captured only after content script injection and are bounded.

  • Resource summaries are counts from the Performance API only; request headers, cookies, storage, history, bookmarks, and downloads are intentionally not exposed. Response bodies are not available through page_status.

  • Browser history, bookmark, download, and cookie tools are intentionally not exposed.

Security

  • No default token is accepted. Set CHROME_BROWSER_CONTROL_TOKEN to a high-entropy URL-safe value for both the broker and MCP adapter, then paste the same value into the extension popup.

  • The broker binds only to loopback hosts: 127.0.0.1, localhost, or ::1.

  • The extension only connects to ws://127.0.0.1, ws://localhost, or ws://[::1] with an optional port.

  • Page access is limited by allowed origins configured in the popup. Use explicit entries such as https://example.com, or enter * to allow all normal http:// and https:// web pages. Tabs and page actions outside the configured scope are blocked.

  • Allowed-origin checks happen in the extension background before content actions, screenshots, and tab claims.

  • Password-like and OTP fields are detected by input type, autocomplete, names, IDs, labels, and placeholders. type blocks them unless force=true.

  • Optional CHROME_BROWSER_CONTROL_EXTENSION_ID pins the broker to one installed extension ID.

  • Trusted CDP input is opt-in and off by default. The debugger permission is required in the manifest, so Chrome shows a warning when the extension loads. The extension always holds debugger capability; it cannot be revoked without uninstalling. Enable the popup enableCdp toggle, claim the tab, then call cdp_attach. The debugging banner on attached tabs is expected. Attach is gated by that stored flag plus a claimed tab plus the existing allowed-origins list; the method allowlist is Input.dispatchMouseEvent, Input.dispatchKeyEvent, Network.enable, Network.disable, and Network.getResponseBody. Response-body reads are a separate deny-by-default allowlist in the popup; that list does not accept *. Restricted categories (banking, wallet, password manager) are refused even when listed. This is an opt-in debugging tier for someone who understands what they pointed it at. It is not safe to switch on and leave on. The socket fails closed on service-worker suspension, navigation away from an allowed origin, DevTools eviction, the popup toggle turning off, and Allowed Origins changes that leave the tab outside the list. Hydration never treats another debugger's attached target as this extension. The MCP adapter does not open a raw CDP socket.

Never bind the broker to a non-loopback interface or commit tokens, local config files, logs, or personal setup notes.

Maintainer publish

First public npm releases are manual. Maintainers follow docs/publish-checklist.md. Do not add auto-publish-on-push or long-lived npm tokens in CI for the default release path.

Available Tools

30 tools
activate_tabActivate Chrome tabA

Focus an allowed tab and its window without navigating. Waits until the document reports visibilityState visible or a short timeout elapses. Returns visibilityState and visible; reason is present only when visible is false. focused is not success. Use this when click/type fail with DOCUMENT_HIDDEN. Pass allowHidden=true only when visibilityState is hidden. If reason is host_permission_denied or document_unavailable, grant permission or reload; allowHidden will not help. Does not change the page URL.

Inputs: tabId, sessionTabId.

browser_statusBrowser bridge statusA

Check whether the MCP adapter can reach the local broker and whether the Chrome extension answers ping. Read nextAction for onboarding coaching. When the extension answers, also read cdpEnabled and attachedTabs for the trusted-input tier.

Inputs: no named parameters.

cdp_attachAttach trusted CDP inputA

Attach trusted CDP input to a claimed tab. While attached, click, type, keypress, click_at, and matching perform_actions steps use trusted CDP input. Requires the popup enableCdp toggle, a claimed tab, and an allowed origin. The debugger permission is required in the manifest. Chrome shows a debugging banner on the tab. Repeating attach on the same tab refreshes the TTL without tearing down the socket.

Inputs: sessionTabId (required), ttlMs.

cdp_detachDetach trusted CDP inputB

Detach trusted CDP input from a claimed tab and return click/type/keypress to the content-script path. release_tab and finalize_tabs also detach, including kept finalize claims.

Inputs: tabId, sessionTabId.

cdp_network_requestsList watched network requestsA

Return the in-memory network request index for an attached tab. Rows are metadata only. Response headers, Set-Cookie, and request post data are never included.

Inputs: sessionTabId (required).

cdp_network_watchWatch network requestsA

Enable the Network domain on an attached tab and keep an in-memory index of metadata rows only (requestId, url, method, status, mimeType, size, timestamp). No response body is read or stored. Restricted-category origins never enter the index. Requires cdp_attach. Optional patterns narrow which URLs are indexed.

Inputs: sessionTabId (required), patterns.

cdp_response_bodyRead one response bodyA

Read one response body by requestId from an attached tab. Deny by default: the request origin must be in the popup body-capture allowlist, which does not accept *. Restricted-category origins are refused even when listed. Binary bodies (base64Encoded) are refused. Bodies over the size cap return an error, never a truncated body. Token-shaped JSON fields receive best-effort masking of obvious token-shaped fields, not a guarantee — treat any response body as if it contains credentials. Response headers, Set-Cookie, and Network.getRequestPostData are never exposed. Each successful read is recorded to an append-only metadata log; body content is never written there.

Inputs: requestId (required), sessionTabId (required).

claim_tabClaim Chrome tabA

Claim an allowed Chrome tab for this browser-control session. Advisory claims are default. Use exclusive=true with ttlMs for fail-fast tab leases across parallel agents.

Inputs: tabId (required), owner, ttlMs, exclusive.

clickClick page elementA

Click an element by snapshot ref in the current top document or an exact documentId from list_frames. Fails with DOCUMENT_HIDDEN when the document is hidden unless allowHidden=true.

Inputs: ref (required), after, tabId, documentId, +2 more.

click_atClick viewport coordinatesA

Click at tabViewport coordinates in the top document or frameViewport coordinates in an exact iframe documentId. Fails with DOCUMENT_HIDDEN when the document is hidden unless allowHidden=true.

Inputs: x (required), y (required), after, tabId, +3 more.

collect_scrollCollect while scrollingA

Scroll a bounded number of steps (hard ceiling when until is set), extract selected elements each step, optionally target a nested scroll container, and optionally stop early via until conditions. Results include stoppedReason.

Inputs: steps (required), extract (required), after, tabId, +8 more.

console_logsConsole logsC

Return bounded console logs captured after the content script was injected in the target tab.

Inputs: limit, tabId, levels, documentId, +1 more.

extract_elementsExtract page elementsB

Extract bounded structured data from elements selected by CSS selector. Safer alternative to raw JavaScript evaluation.

Inputs: selector (required), limit, tabId, visible, +6 more.

extract_feed_postsExtract feed postsB

Extract structured feed/post records (author, text, times, live flags) from a scoped feed region without site-specific selectors.

Inputs: scope, tabId, maxPosts, documentId, +3 more.

finalize_tabsFinalize claimed tabsA

Release browser-control ownership state for claimed tabs. This does not close user tabs; pass keep entries to preserve handoff/deliverable claims. Detaches CDP attachments on every claimed tab, including kept claims.

Inputs: keep.

keypressPress page keysB

Dispatch common DOM keyboard events in the target page. Browser/OS shortcuts are not guaranteed under MV3. Fails with DOCUMENT_HIDDEN when the document is hidden unless allowHidden=true.

Inputs: keys (required), after, tabId, documentId, +2 more.

list_framesList frame documentsA

List current frame documents for an allowed target tab. Blocked or unsupported frames are redacted and cannot be targeted.

Inputs: tabId, sessionTabId.

list_tabsList Chrome tabsA

List tabs visible to the Chrome Browser Control extension in the current Chrome profile.

Inputs: no named parameters.

name_sessionName browser sessionA

Set a human-readable browser-control session name for status, logs, and debugging.

Inputs: name (required).

page_statusPage statusC

Return lightweight page status, viewport/scroll state, and resource summary counts for an allowed target tab.

Inputs: tabId, documentId, sessionTabId.

perform_actionsPerform sequential page actionsA

Run up to 10 sequential page actions in one document target. An explicit batch-level documentId is revalidated before every step and after observation; steps cannot override it. Fail-fast on the first step error. Click, type, and keypress steps fail with DOCUMENT_HIDDEN on hidden documents unless that step sets allowHidden=true.

Inputs: actions (required), after, tabId, documentId, +1 more.

query_elementsQuery page elementsB

Find elements by selector, role, text, and visibility without returning full page text.

Inputs: role, text, limit, tabId, +4 more.

release_tabRelease claimed tabA

Release a previously claimed tab by sessionTabId or tabId without closing the browser tab.

Inputs: tabId, sessionTabId.

screenshotCapture visible screenshotA

Capture the visible tab viewport. This tool is tab-target only; iframe frameViewport bounds are not valid crop coordinates. Optionally crop to a top-document snapshot ref or tabViewport bounds.

Inputs: ref, tabId, bounds, format, +2 more.

scrollScroll pageA

Scroll the current top document or an exact documentId by pixel deltas. x/y are tabViewport coordinates for the top document and frameViewport coordinates for an iframe. Use textLimit on snapshot to capture more text; scroll only helps when the page lazy-loads content.

Inputs: x, y, after, tabId, +4 more.

snapshotSnapshot active pageA

Return a simplified DOM snapshot for the current top document or an exact documentId from list_frames. Compact mode (default) returns textPreview only — not text. Full mode returns text. Compact defaults to main-landmark scope when present; a visible modal dialog (aria-modal=true or opened with showModal()) takes scope instead. Pass scope: "document" for legacy full-body text including the page behind a modal, ignoreRoles: ["dialog"] or ["alertdialog"] to hide both dialog and alertdialog, or mode: "full" for the unscoped legacy snapshot. Defaults truncate at 500 (compact) or 4000 (full) chars; pass textLimit (up to 100000) for long page content such as API docs. Response includes authoritative document identity and coordinate space.

Inputs: mode, scope, tabId, textLimit, +4 more.

typeType into page elementA

Type text into an element by snapshot ref. Password-like fields are blocked unless force=true. Fails with DOCUMENT_HIDDEN when the document is hidden unless allowHidden=true.

Inputs: ref (required), text (required), after, force, +4 more.

visible_snapshotVisible page snapshotB

Return a viewport-aware snapshot with visible/intersecting elements, refs, labels, roles, bounds, and scroll metadata. Iframe bounds use frameViewport coordinates.

Inputs: limit, tabId, documentId, sessionTabId.

wait_forWait for page conditionB

Wait for text, selector, URL substring, selector absence, scoped text, or bounded content stability in the target page.

Inputs: text, scope, tabId, selector, +9 more.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    B3.2/5.0

    Scored across 30 tools

    Disambiguation2/5

    Several tools overlap significantly: snapshot, visible_snapshot, query_elements, extract_elements, and page_status all provide ways to inspect page content/state; click, click_at, and perform_actions (with click steps) overlap in interaction; browser_status and page_status both report status but with different focus. The descriptions are detailed but the high-level purposes blur together.

    Naming Consistency3/5

    Tool names are predominantly verb_noun (e.g., list_tabs, navigate, click_at), but there is inconsistency in verb style: some use explicit verbs like 'browser_status' (noun-only), 'cdp_attach' (abbreviation), 'snapshot' (noun-only), and 'keypress' (verb-noun but single word). Also 'extract_feed_posts' is more domain-specific than the rest.

    Tool Count2/5

    30 tools is on the high end for a browser control server. While the domain is complex, many tools feel like slight variations of each other (e.g., snapshot vs visible_snapshot), making the count feel bloated. The server could be consolidated to around 20 tools without losing functionality.

    Completeness4/5

    The tool set covers well the lifecycle of controlling a browser: claiming tabs, navigating, interacting, snapshotting, waiting, extracting, and CDP network inspection. Missing basic operations like page reload, back/forward, or download management are minor gaps that agents can work around.

    Maintenance

    ActivityNo data
    ResponsivenessOutdated data

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables browser automation over MCP using a real Chrome browser with existing profile, supporting real tabs, downloads, cookies, and RPA workflows.
      166 npm
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables local opencode agents to control a live Chrome browser via MCP tools, including tab management, JavaScript execution, clicking, form filling, page reading, screenshots, and console log retrieval.
      2
      MIT
    • A
      license
      Not graded
      quality
      A
      maintenance
      Enables remote agents to control a local Chrome/Edge browser via MCP, supporting navigation, snapshots, clicks, typing, screenshots, and script execution.
      25 npm
      ISC
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables remote AI agents to control a local, authenticated desktop browser over MCP through an outbound WebSocket bridge and Chrome extension, supporting tab management, navigation, page reading, clicking, typing, scrolling, screenshots, and JavaScript evaluation.
      1
      MIT