Browser Tool (Chrome DevTools MCP)
Peekaboo exposes a native browser tool that brokers Chrome DevTools MCP. Agents call it through MCP, and scripts can use the dedicated peekaboo browser CLI wrapper. Use it for Chrome page content:
- DOM/accessibility snapshots
- page-level click/fill/type/navigation
- console and network inspection
- page screenshots
- performance traces
Use Peekaboo native tools for macOS UI, browser chrome, menus, dialogs, permissions, window management, and non-browser apps.
#Provider update guard
The provider is pinned to 1.9.0, including bundled Puppeteer 25.10.0 and MCP SDK 1.30.0. Its unmodified handler calls context.getDevToolsData(page) after every tool even when usage statistics are disabled. With DevTools open, this calls hasDevTools(), openDevTools(), and DevTools-page Puppeteer evaluation with CDP userGesture: true. An otherwise background-safe console read can therefore grant web user activation in DevTools. Disabling JavaScript tools or telemetry transmission does not remove the post-handler probe.
BrowserMCPProviderBootstrap applies an in-memory telemetry fix: collect those two metadata fields only when ClearcutLogger.get() returns an active logger. It verifies the package version and SHA-256 of the complete published ToolHandler.js before loading the module, and refuses changed bytes before server startup or browser connection. The bootstrap is embedded in the native CLI and GUI host, so the separately downloaded npx provider receives the fix too. It does not edit npm caches, suppress tool-owned DevTools reads, change Puppeteer user gestures, or weaken Peekaboo's foreground policy. Node's module loader registration supports the provider's existing runtime minimum; no extra dependency is needed.
The same bootstrap verifies browser.js and the bundled dependency module before disabling automatic reconnection and WebSocket redirects. Its private owner tool obtains Browser.getVersion through the provider's retained socket. Redirects must remain disabled: Puppeteer otherwise follows a redirect while continuing to report the original WebSocket URL, which would misattribute the resulting connection. The synthetic socket fixture exercises the real bundled Puppeteer, including refusal, reuse, disconnect, and redirect rejection.
The dependency contract test executes this same bootstrap, reproduces the unpatched failure, requires zero metadata reads with disabled telemetry on both success and error paths, and verifies enabled telemetry and tool-owned reads. It also rejects changed handler bytes, audits every tool's routing/UID/response contract, and starts the full launcher without attaching Chrome. Remove the patch only when an upstream release fixes this path and passes the complete routing, UID, and user-activation audit. The 1.9.0 handler and page helper own the behavior; changing Peekaboo's background policy would weaken the existing contract.
The audit covers all 58 upstream tools, while public raw-call schemas advertise only the 29 tools registered by this provider configuration. Optional PWA tools require a category that conflicts with an existing-browser connection; those and disabled experimental tools are not advertised as usable capabilities.
#Permission flow
Peekaboo attaches to an already-running Chrome profile. It requires:
- Chrome 144 or newer.
- Chrome running locally.
- Remote debugging enabled at
chrome://inspect/#remote-debugging. - User approval in Chrome's remote debugging permission prompt.
Peekaboo recognizes running channels by exact Chrome bundle identifier, so app-owned helper or XPC service names cannot be mistaken for another browser process. Native channel mode also requires the live process to satisfy Google's Apple-anchored code-signing requirement for Team ID EQHXZ8M8AV. Peekaboo pins its exact signed channel identifier, Team ID, and CDHash to the PID generation before opening the approval-gated WebSocket, then requires the same identity after Browser.getVersion and around every later listener revalidation. Peekaboo does not approve the prompt automatically. Once Chrome publishes DevToolsActivePort, channel connect reads that owner-controlled file without following symlinks, proves that its one exact loopback listener belongs to the detected Chrome PID and process generation, and passes the exact published WebSocket as --wsEndpoint to Chrome DevTools MCP. The provider opens one connection, waits for Chrome's approval, and answers a private owner verification request with CDP Browser.getVersion on that same connection. Peekaboo revalidates the process-owned listener and signer before publishing a complete receipt. Approval and provider startup share the 90-second connection deadline. Page operations reuse this socket; there is no separate approval probe. Exact-WebSocket providers use a 90-second MCP request ceiling so stdio RPC cannot cut off Chrome's 60-second approval upgrade. The owner still enforces the shared connection deadline; individual tool timeouts remain provider-controlled. Once the child is connected, status, repeated connect, and browser execution revalidate the active-port file, kernel listener, PID generation, and bundle without opening another native WebSocket or prompting again. Peekaboo never uses legacy HTTP discovery for channel mode or asks the MCP child to rediscover an ambient browser.
Chrome's approval-mode listener deliberately returns HTTP 404 for /json/version. This does not mean that the WebSocket is unavailable: use browser connect --channel stable --foreground for the standard profile. Explicit --browser-url requires a listener that implements HTTP discovery, as normally used by a separate custom profile. --remote-debugging-pipe alone exposes process pipes, not a TCP discovery endpoint. Do not relaunch the user's profile, switch to an unrelated managed Chrome port, or add a wildcard origin allowance to repair this 404. The chrome://inspect/#remote-debugging setting enables approval-mode access; Chrome still asks for permission per connection. A refused, cancelled, or disconnected connection is never silently retried by the provider.
CLI page commands reuse the root connection's exact receipt. A provider epoch in root status is diagnostic; the client includes epoch authority only in authenticated scoped-session requests, where the host enforces it.
#Privacy defaults
Peekaboo starts Chrome DevTools MCP with:
# Effective provider options; Peekaboo's embedded bootstrap applies the audited telemetry fix first.
chrome-devtools-mcp \
--wsEndpoint=ws://127.0.0.1:<port>/devtools/browser/<id> \
--page-id-routing \
--experimentalStructuredContent \
--no-usage-statistics \
--no-performance-crux
Peekaboo pins the verified Chrome DevTools MCP version because direct page-ID routing and the structured response data used to mint opaque page/element capabilities remain version-sensitive upstream contracts. Upgrade the pin only after its page-scoped schemas, structured response surfaces, and routing behavior have been revalidated.
For deterministic legacy CLI tests or custom Chrome endpoints:
PEEKABOO_BROWSER_MCP_ISOLATED=1lets the standalone browser CLI launch a temporary Chrome profile. AuthenticatedPEEKABOO_BROWSER_MCP_HEADLESS=1makes that launched browser headless.PEEKABOO_BROWSER_MCP_BROWSER_URL=http://127.0.0.1:9222connects to an explicit debuggable Chrome endpoint instead of auto-connect.
MCP and Agent capability sessions reject this mode before provider startup because the child does not expose a browser identity that Peekaboo can bind to caller-owned opaque references.
The CLI exposes the safer request-carried equivalent:
peekaboo browser connect --browser-url http://127.0.0.1:9222 --foreground --json
Only loopback HTTP endpoints with a port from 1 through 65535 are accepted. Use brackets for IPv6, for example http://[::1]:9222. IPv6 loopback, IPv4 loopback, and localhost retain separate listener identities. This explicit-URL mode resolves /json/version, pins the returned browser WebSocket identity, probes list_pages before reporting connected, and revalidates that identity before every later tool call. It is the compatibility path for custom or non-Google-signed debuggable browsers; unlike native channel discovery, it does not claim a Google code-signing identity or process-bound channel receipt. When multiple Chrome processes share one channel, channel-only connection refuses and requires this exact endpoint. Channel discovery reads only the standard current-user profile for the chosen Chrome channel; a headless or custom profile cannot substitute an arbitrary authority file for a detected GUI browser.
The tool can expose page content, cookies/session-backed data visible to the page, console messages, network requests, screenshots, and traces to the active agent/MCP client. Do not enable it for browser profiles containing sensitive data unless that exposure is acceptable.
Browser uploads are host-owned. Peekaboo gives each Chrome DevTools MCP child one private owner-only temporary root and does not pass --allowUnrestrictedPaths. Every mapped or raw upload_file call is intercepted before dispatch: the source must be an absolute, current-user-owned regular file no larger than 100 MiB, opened without following a final symlink, and copied from that checked descriptor into a read-only transfer directory under the child root. The copy is kept for the exact browser session because Chrome can read an attached file after the upload tool returns; it is removed only after the MCP child terminates on disconnect, connection loss, endpoint drift, or cancellation. Peekaboo's public single-file filePath contract is translated to the provider's filePaths array only after staging; caller-supplied filePaths arrays are refused before dispatch. External upload authorization remains the caller's responsibility.
#Persistence
Browser MCP state is owned by BrowserMCPService through BrowserMCPSessionManager.
Native browser discovery and live process-bundle reads execute on a dedicated background queue; no AppKit application objects cross back to MainActor. Explicit browser status still waits for the session gate and validates the connection. Daemon health instead returns cached diagnostics marked indeterminate, with at most one refresh outstanding. An uninitialized or stalled refresh cannot confirm browser absence, and cached receipts never replace fresh execution validation.
- In a local MCP process, the browser tool uses the
BrowserMCPServicefromMCPToolContext. Public MCP and - In Bridge-backed MCP mode, an explicit
--browser-handoff <absolute-private-path>plus one exact--bridge-socket - The selected runtime host owns the
chrome-devtools-mcpchild process and per-page snapshot UID state. - Separate legacy CLI invocations require the same current-build reusable daemon. Bridge-scoped handoff instead
- Native channel connections and explicit loopback URLs both resolve to an exact WebSocket and are eligible for
- Child-process loss, PID reuse, endpoint restart, or an attempted retarget fails closed with reconnect guidance. Peekaboo
- Receipt-bound execution resolves a complete WebSocket browser identity and compares that identity inside the browser
- Active upload cancellation terminates the exact MCP child before deleting its private transfer root; an operation ID
- Peekaboo enables Chrome DevTools MCP's page-ID routing. Every page-scoped action requires
page_idand is - Persistent MCP and Agent sessions, including authenticated Bridge-scoped sessions, never receive Chrome's
- Independently authenticated process-local browser sessions own separate Chrome DevTools MCP children and FIFO
- On the selected runtime host, each browser-enabled
peekaboo mcp servesession, or server consuming an explicit
standalone Browser contexts default to background-only and require an existing live exact connection receipt; they never auto-connect implicitly. The pinned provider grants browser user activation to every Puppeteer page evaluation, including internal page-title, stable-DOM, snapshot, and element-geometry work. Background contexts therefore expose only source-audited routes that cannot reach that path and refuse the rest before provider I/O.
opens an authenticated caller-scoped transport. A foreground peekaboo browser connect --handoff-file on that same socket first writes the canonical signed receipt. The Bridge consumes it once, validates the caller, listener/host generation, exact target receipt, claim, and provider epoch, then creates a distinct scoped Chrome DevTools MCP child. Scoped status, execution, disconnect, and terminal end all carry the opaque session ID. Status returns the provider epoch, and execution re-presents that epoch with the exact connection receipt; the remote client remints page and element references for its caller and never falls back to the root connection.
requires a current host advertising authenticated browser-session bootstrap/control; older hosts reject it before the MCP server starts.
receipt-bound execution. Isolated-profile children remain unbound because the child does not report a pinnable browser identity.
never silently rediscovers another same-channel profile.
execution gate before the first call. Channel receipts additionally carry the owning PID, process generation, and exact Chrome bundle identity. Bridge protocol 1.34 and the nativeBrowserConnectionBinding capability gate this combined process-and-DevTools contract; downgraded hosts may still use an explicit loopback browser_url but cannot perform channel discovery or accept a process-bound browser receipt. Calls that can reach provider evaluation require explicit foreground authority and report browser-protocol foreground delivery even when Chrome remains visually behind another app. Multi-call responses retain exact completed and dispatched-or-accepted counts; a later failure returns a typed retry-unsafe outcome so callers resume only after observation, never by replaying the whole batch.
prevents delayed cancellation cleanup from terminating a newer browser session.
routed directly to that page instead of relying on the process-global selected page. The upstream MCP server serializes calls with its FIFO tool mutex, so concurrent agents cannot redirect one another between selection and execution.
process-local page integers or snapshot-local UIDs as mutation authority. list_pages and new_page project opaque caller-owned page references, and browser snapshots project opaque element references bound to the exact connection, MCP child epoch, backend page, navigation generation, and provider node, frame, loader, or navigation identity when Chrome supplies it. A newer snapshot invalidates prior element refs; navigation, disconnect, connection replacement, and MCP-session teardown invalidate their complete subordinate namespace. Before element dispatch, the same provider gate takes a fresh snapshot and proves every provider UID is still present in the current document. References copied into another caller session fail before Chrome dispatch.
execution/mutation gates, so one blocked session does not stall another while calls within each session remain ordered. Peekaboo reserves the canonical process/DevTools target before permission-bearing provider setup; two sessions therefore cannot even transiently connect or probe the same exact target. Authenticated sessions refuse isolated-profile children before provider startup because their intentionally receiptless browser identity cannot support caller-owned opaque references. Launch a separate headless Chrome and connect through its exact loopback browser_url when a non-GUI browser is required. Foreground-capable browser setup/activation remains on the shared desktop lane, and failed snapshot invalidation is kept in one ordered cross-session ledger that every browser and desktop mutation must drain before dispatch. Bridge-backed sessions use the authenticated browser-session namespace: each claim owns its provider child, exact target receipt, provider epoch, opaque-reference namespace, and cleanup lifecycle. Ambiguous opens retry only with the same claim and payload; ambiguous end cleanup retains the exact handle until confirmed, while wrong-owner or host-generation failures become terminal without unsafe replay.
browser handoff, owns and tears down its own browser child. Without an authenticated Bridge handoff, the background-only default starts disconnected and cannot bootstrap browser control. To authorize setup for that exact server-owned child, start peekaboo mcp serve --allow-foreground and invoke its browser connect action; user-activating page operations require the same explicit server authority and use the resulting caller-owned connection. This scoped connection must use a native Chrome channel or an exact loopback browser_url; isolated mode is reserved for legacy standalone CLI sessions. With a Bridge handoff, the background server instead starts bound to the transferred exact connection and does not expose connect.
Use peekaboo daemon status to see browser connection state, tool count, and detected Chrome channels.
#Actions
Common actions:
statusconnectdisconnectlist_pagesselect_pagenew_pagenavigatewait_forsnapshotclickdom_clickfilltypepress_keyconsolenetworkscreenshotperformance_trace
Advanced escape hatch:
callwithmcp_toolandmcp_args_jsonforwards a raw tool from the audited, pinned Chrome DevTools MCP
v1.9.0 catalog. Page-targeted raw tools require the wrapper's top-level page_id; Peekaboo validates and injects it as upstream pageId, overriding any nested value in mcp_args_json. Truly global tools such as list_pages reject page_id. UID-bearing raw schemas are resolved only at their audited positions, including evaluate_script.args, form elements, and third-party singleton { "uid": ... } parameters; unrelated domain fields named uid remain data. Raw page-list and snapshot responses receive the same opaque projection. Snapshot file output is refused in capability sessions because the provider artifact would contain unprojected UIDs. Error, non-snapshot success, content-item metadata/resources, status-prefixed, structured-message, fallback-page-note, and upload responses project their provider-owned fields too, without rewriting page titles, script values, or other domain data. Provider page/UID tokens and private staging paths therefore never become caller authority or diagnostics. Provider error payloads are reduced to a safe generic diagnostic while retaining Peekaboo's canonical outcome metadata, because arbitrary error objects cannot distinguish provider identifiers from domain data. Text-only snapshots never mint element capabilities: without a structured snapshot, page-controlled multiline accessibility text makes line-leading uid= rows fundamentally ambiguous, so Peekaboo fails closed. trigger_extension_action is audited but blocked because upstream still resolves its shared selected page internally; Peekaboo will not forward it until upstream supports explicit pageId routing. Unknown raw tool names fail closed until the routing contract is audited and updated. Background schemas advertise only raw routes with a request-provable no-user-activation variant. Any route that can reach Puppeteer evaluation—including evaluate_script, page-list formatting, snapshots, waits, or element work—is hidden and refused before provider entry. Use explicit CLI --foreground, MCP --allow-foreground, or Agent --allow-foreground authority when those operations are intended.
In a foreground-authorized MCP or Agent session, start page work with list_pages or new_page, retain the returned opaque page reference, and include it as page_id in every later page-scoped action. Retain element references only from the newest snapshot for that page. The standalone CLI continues to expose the provider page integer because each CLI invocation is an explicit compatibility boundary rather than a persistent caller capability namespace. select_page and new_page can keep the page visually behind other apps, but remain foreground-authority operations because their provider response formatting grants browser user activation. Use bring_to_front: true or background: false only when visible foreground interaction is intentional.
dom_click invokes one synthetic element.click() via evaluate_script, without CDP/Puppeteer pointer input. It uses the existing exact connection receipt, caller-owned page and element references, provider child epoch, and fresh element preflight under the same execution gate. Newer snapshots, navigation, session end, or connection/epoch replacement invalidate the relevant references; copied references from another caller never authorize dispatch. The pinned evaluation route still grants browser user activation. Background sessions hide and refuse dom_click before provider I/O; explicit foreground sessions report foreground browser-protocol delivery. This is not a background-safe route or a guarantee that Chrome cannot activate.
Synthetic click is not trusted pointer input: it does not move or hit-test the pointer or emit a pointer-down/up sequence, disabled controls may do nothing, and handlers requiring trusted input may not react. double and include_snapshot do not apply. A successful script return is not final-effect proof: inspect the intended page afterward to confirm the handler or navigation actually took effect. Accepted or uncertain dispatch remains retry-unsafe; do not blindly replay a click after failure or cancellation.
type and press_key also require an opaque element reference from the newest snapshot as uid. Peekaboo holds one browser execution gate while it focuses that exact uid and sends the keyboard operation; concurrent page work cannot interleave between those leaves.
#Examples
CLI:
peekaboo browser status --json
peekaboo browser connect --channel stable --foreground
peekaboo browser connect --browser-url http://127.0.0.1:9222 --foreground
peekaboo browser new-page --url https://example.com --foreground
peekaboo browser navigate --page-id 2 --url https://example.com/docs --foreground
peekaboo browser snapshot --page-id 2 --path /tmp/page.txt --foreground
peekaboo browser network --page-id 2 --request-id 7 --json
MCP JSON:
{ "action": "status" }
{ "action": "connect", "channel": "stable" }
The MCP server is background-only and refuses that connect request before dispatch. To share a connection, route both the explicit foreground CLI connection and the MCP server to the same reusable daemon:
peekaboo daemon start
peekaboo browser connect --channel stable --foreground \
--bridge-socket "$HOME/Library/Application Support/Peekaboo/daemon.sock"
peekaboo mcp serve \
--bridge-socket "$HOME/Library/Application Support/Peekaboo/daemon.sock"
A default process-local peekaboo mcp serve cannot reuse browser state created by a separate CLI process.
{ "action": "snapshot", "page_id": "bp1_<opaque>" }
{ "action": "fill", "page_id": "bp1_<opaque>", "uid": "be1_<opaque>", "value": "[email protected]", "include_snapshot": true }
{ "action": "network", "page_id": "bp1_<opaque>", "page_size": 20, "resource_types": ["xhr", "fetch"] }
{ "action": "performance_trace", "page_id": "bp1_<opaque>", "trace_action": "start", "reload": true, "auto_stop": true }