chrome-devtools 1.5.1

Profile-aware CLI for running Chrome DevTools MCP with isolated Chrome user data directories
---
name: chrome-devtools-profile-control
description: Use the chrome-devtools CLI confidently to drive Chrome DevTools MCP through explicit browser profiles, persistent MCP sessions, and JSON-RPC tool calls. Trigger this skill when the user asks to operate Chrome, use Chrome DevTools MCP, open pages, inspect browser state, or perform browser UI actions through chrome-devtools.
---

# Chrome DevTools Profile Control

Use `chrome-devtools` when you need browser automation through Chrome DevTools MCP and the user expects actions to happen in a named Chrome profile.

## Core Rules

- Always choose an explicit profile with `--profile <name>`. Do not rely on an implicit browser instance.
- **Mint a session before running `mcp call` or `mcp batch`.** Both subcommands require `--session <id>` and refuse to start without it. Use `chrome-devtools session create --profile <name>` to get an id, then reuse the same id for every related invocation.
- **For multi-step automation, prefer `chrome-devtools mcp batch --profile <name> --session <id> --script <path>`.** It executes a JSON scenario through the daemon and returns a JSON array of results, which is far easier to parse than line-delimited JSON-RPC.
- For ad-hoc/interactive flows (especially `take_snapshot` → inspect uids → follow-up tool call in the same shell session), use `chrome-devtools mcp call --profile <name> --session <id>` with stdin JSON-RPC.
- Both `mcp batch` and `mcp call` route through one long-lived per-profile daemon. The daemon assigns each session to a page and injects `pageId` for page-scoped tools.
- Sessions expire after 30 minutes of inactivity. Activity (any tool call) refreshes the timer. After expiry, mint a new id.
- A session is held exclusively while bound by a `mcp call` / `mcp batch` invocation. A second concurrent invocation on the same session id is rejected by the daemon. Different session ids may run concurrently.
- `take_snapshot` result `uid` values are session uid tokens. Reuse them only in the same session and after the latest snapshot.
- Do not use `mcp direct-call` for a split `take_snapshot` then `click`/`fill` workflow; direct mode starts a separate MCP process and the snapshot cache is lost.
- Take a fresh snapshot before using `click`, `fill`, or other uid-based actions if the page may have changed.
- On very large pages (huge DOM, e.g. big GitHub PR file views) `take_snapshot` can take minutes: Chrome's full accessibility-tree dump scales super-linearly with node count. Heavy tools (`take_snapshot`, `take_screenshot`, `performance_*`) get a 300s deadline (others 120s). If a heavy call times out it usually still completed in the background and blocks the next tool call until done — prefer targeted `evaluate_script` over full snapshots on such pages.
- Do not start or kill the user's regular Chrome unless explicitly asked.
- If a browser login is required, open the page and ask the user to complete the login manually.
- The daemon and its Chrome are shared with other agents. `daemon stop` refuses while sessions are active and `profile stop` refuses while the daemon is running; only pass `--force` when the user explicitly asks to tear them down.
- If Chrome's DevTools endpoint or MCP stops responding, the daemon respawns MCP and drops in-memory sessions. Mint a new session after an `unknown session` response.

## Tool Argument Notes

- `navigate_page` requires a `type` discriminator: `{"type":"url","url":"...","timeout":30000}`. A bare `{"url":"..."}` fails with "Either URL or a type is required".
- If a tool fails with "The selected page has been closed", start the batch with `list_pages` and open a fresh page via `new_page` (URL argument required) instead of relying on the previously selected page.
- `fill` on a checkbox/toggle takes `value: "true"` / `"false"`. `click` and `fill` accept `includeSnapshot: true` to return a snapshot of the page right after the action.
- `new_page`'s `isolatedContext` argument is stripped automatically (with a warning): isolated contexts are incognito-like and disable extensions. When you need a separate login state, add another profile to `~/.config/chrome-devtools/config.toml` instead.
- File-writing arguments (`take_screenshot` `filePath` etc.) accept paths under the home directory and the OS tempdir.
- `get_network_request --responseFilePath <path>` saves the body with a `.network-response` extension appended to the given path.

## Daemon-Native Tools (no snapshot required)

The daemon adds ten tools of its own on top of the MCP tool set (they appear in `tools/list` and are called like any other tool). They run over CDP directly on the session's page, bypassing the MCP process — they work even while a heavy MCP tool is still running, and they never trigger an accessibility snapshot, so they are the right choice on huge-DOM pages where `take_snapshot` is impractical.

Each session is pinned to the CDP target id of its tab. Navigation, redirects, hash changes and SPA route changes alter the URL but not the target, and tabs that share a URL are told apart, because `new_page` and `select_page` pin the tab's target id when the session takes it. These tools never fall back to another tab: when the session's tab is gone they fail with `session page target <id> is gone; create a new session`, and a page that cannot be pinned fails with `could not tell which tab is the session page` (run `select_page` again or create a new session). Several agents can therefore drive one Chrome profile at the same time without stealing each other's tabs.

- `goto {url, waitSelector?, waitExpression?, timeout?, interval?}` — navigate the session page without foregrounding Chrome, then wait in the same tool call. `waitSelector` and `waitExpression` are mutually exclusive; without either it waits for `document.readyState === 'complete'`. When the condition is not met in time, the error carries the current URL, title, `readyState` and what the condition evaluated to (`wait_for_js` reports the same).
- `screenshot_quiet {filePath, format?, quality?}` — save a visible-viewport screenshot directly through CDP without selecting the page or entering the MCP queue. Use this for Slack/image viewers. `filePath` must be absolute and under `$HOME` or the OS temp directory.
- `wait_for_js {expression | selector, timeout?, interval?}` — poll a JS expression (or CSS selector) until truthy. Use instead of fixed `sleep_ms` waits for SPA rendering. Defaults: timeout 30000ms (max 120000), interval 500ms. A throwing/invalid expression fails immediately rather than polling.
- `click_selector {selector}` — scroll the first matching element into view and click its center. Inside iframes the click point is translated to the top frame's viewport.
- `click_at {x, y}` — raw left click at viewport coordinates.
- `dispatch_key {key, modifiers?}` — single key press (`Escape`, `Enter`, `Tab`, `ArrowDown`, single characters, ...). Modifiers bitmask: Alt=1, Ctrl=2, Meta=4, Shift=8.
- `set_file_input {selector, filePaths}` — attach files to the `input[type=file]` matched by the selector via `DOM.setFileInputFiles`. Prefer it over `upload_file` when a page has several file inputs (a cover letter field and a resume field, say) or when no visible button opens a chooser. Works on large-DOM pages such as an open Gmail thread. Paths must be absolute and under `$HOME` or the OS temp directory.
- `print_pdf_quiet {filePath, landscape?, printBackground?, scale?}` — save the session page as a PDF through `Page.printToPDF`. `printBackground` defaults to true, `scale` is clamped to 0.1-2.0. `filePath` must be absolute and under `$HOME` or the OS temp directory.
- `close_page_quiet {}` — close the session's own tab through the DevTools HTTP endpoint. Use it to recover when a JavaScript dialog or a tool that never returns is holding the MCP tool mutex, which is exactly when the MCP `close_page` cannot get through. The session gets a fresh tab on its next call.

The daemon watches each session's tab for JavaScript dialogs and closes them on its own: `alert` and `beforeunload` are accepted, `confirm` and `prompt` are dismissed. Look for `dialog_handled` in the daemon log. A dialog left open holds the MCP tool mutex, so this keeps a page's `confirm()` from stalling every session on the profile.
- `type_into {selector, text, clear?}` — focus the matched element and insert the text with `Input.insertText`. Use it when `evaluate_script` with a native value setter plus an `input` event leaves the framework state unchanged. `clear: true` selects all and deletes first.

`selector` of `click_selector`, `set_file_input` and `type_into` reaches into iframes with `>>>`: `iframe#form >>> input[name=email]`, nested as `iframe#a >>> iframe#b >>> input`. Same-origin and cross-origin (out-of-process) iframes both work. When an element is missing, the error names the frame, its URL and the iframes it contains (`no element matches selector: #x in frame iframe#form (url ..., iframes here: iframe#inner)`); `could not focus selector` also says whether the matched element is hidden, disabled or read-only.

## Profile Setup

Profiles are configured in `~/.config/chrome-devtools/config.toml`.

```toml
[[profiles]]
name = "default"

[[profiles]]
name = "conao3"
```

`user_data_dir` is optional. When omitted, the CLI uses:

```text
~/.config/chrome-devtools/profiles/<profile-name>
```

Check profiles before operating:

```sh
chrome-devtools profile list
chrome-devtools profile status --profile conao3
```

## Session Lifecycle

Mint, reuse, and tear down sessions explicitly. Every `mcp call` and `mcp batch` invocation must carry `--session <id>`.

```sh
# Mint a session. Output: session=sess-xxxxxxxxxxxxxxxx created=<ts> last_used=<ts> owned=false
chrome-devtools session create --profile conao3

# Capture the id for later use
SESSION=$(chrome-devtools session create --profile conao3 | awk -F= '{print $2}' | awk '{print $1}')

# Inspect active sessions
chrome-devtools session list --profile conao3

# Drop a session when finished
chrome-devtools session close --profile conao3 --session "$SESSION"
```

Sessions are in-memory on the daemon. They are dropped after 30 minutes of inactivity or when the daemon stops (`chrome-devtools daemon stop --profile <name>`). After expiry, mint a new id. When a session first uses a page-scoped tool, the daemon assigns it a background page and routes later page-scoped calls to that page. `session close` and expiry close daemon-created pages when Chrome has another tab; attached pages stay open.

## Batch Execution (Preferred for Multi-Step Workflows)

`mcp batch` reads a JSON array of steps from `--script`, executes them in order through one daemon session, and prints a JSON array of per-step results. Use it whenever the steps are known up front.

### Step shapes

- Tool call: `{ "type": "tool", "name": "<mcp-tool>", "args": { ... }, "label": "<optional>", "on_error": "continue|stop" }`
- Sleep: `{ "type": "sleep_ms", "ms": <u64>, "label": "<optional>" }`

The script is validated before anything runs: a `tool` step named `sleep_ms` / `sleep` / `wait` (the pause is the `sleep_ms` step type, not a tool) or a step without `type` fails the whole batch with the list of step types, and no step executes.

`label` is preserved verbatim in the output, which makes it easy to grep / jq for specific step results.

### Value references inside args

Replace any `args` value with `{ "$ref": "<label>.<path>" }` to substitute it with a previous step's result. `<path>` is dot-separated; numeric segments index arrays. Example: `{ "$ref": "page.result.content.0.text" }` resolves to the first content text returned by the step labelled `page`.

### Error handling

A tool step is considered to have errored if the MCP response carries a non-null `error` field or the result has `isError: true`, except when the error is `Execution context was destroyed` (a click or script that navigated the page): that step is recorded as `"navigated": true` with `error: null` and `result.isError: false`, so the batch continues and the step must not be retried. By default, the scenario records the error and continues; pass `--fail-fast` (or `"on_error": "stop"` on a step) to abort. The runner still emits the partial results array and exits non-zero on aborts.

When the batch fails before any step runs (e.g. `unknown session`), stdout still receives a JSON array of the shape `[{"type":"error","error":"..."}]`, so piping into `jq` keeps working; check the exit code to detect failure.

Common tool argument mistake: `navigate_page` requires a `type` discriminator, e.g. `{"type":"url","url":"...","timeout":30000}` — a bare `{"url":"..."}` fails with "Either URL or a type is required".

### Other flags

- `--script -` reads the scenario from stdin (useful with heredocs).
- `--output <path>` writes the JSON results to a file instead of stdout.

### Result shape (per step)

- Tool: `{ "type": "tool", "name": "...", "label": "...", "result": <mcp tools/call result>, "error": <mcp error or null> }`
- Sleep: `{ "type": "sleep_ms", "ms": <u64>, "label": "..." }`

### Example: navigate → wait → evaluate

```sh
cat > /tmp/scenario.json <<'JSON'
[
  { "type": "tool", "name": "navigate_page", "args": { "type": "reload", "timeout": 15000 } },
  { "type": "sleep_ms", "ms": 5000, "label": "wait-load" },
  { "type": "tool", "name": "evaluate_script", "label": "title", "args": { "function": "() => document.title" } }
]
JSON

chrome-devtools mcp batch --profile conao3 --session "$SESSION" --script /tmp/scenario.json
```

### Example: snapshot → click reusing the uid

Because all steps share the same daemon session page, a `take_snapshot` step's uid token can be referenced by a later `click` step in the same scenario.

```sh
cat > /tmp/click-scenario.json <<'JSON'
[
  { "type": "tool", "name": "take_snapshot", "args": {} },
  { "type": "tool", "name": "click", "args": { "uid": "u:abc:2:1_8" } }
]
JSON
```

When the target uid token is not known in advance, split the work: run the `take_snapshot` scenario first, parse the response, then build a second scenario that references the resolved uid token.

### Reading results with jq

```sh
chrome-devtools mcp batch --profile conao3 --session "$SESSION" --script /tmp/scenario.json \
  | jq '.[] | select(.label == "title") | .result.content[0].text'
```

## Interactive Workflow with `mcp call`

For exploratory or hand-driven JSON-RPC, `mcp call` forwards stdin lines to the daemon's MCP process and prints responses on stdout. The daemon keeps the MCP process alive across invocations.

```sh
chrome-devtools daemon start --profile conao3
SESSION=$(chrome-devtools session create --profile conao3 | awk -F= '{print $2}' | awk '{print $1}')
chrome-devtools mcp call --profile conao3 --session "$SESSION"
```

Initialize the MCP connection:

```json
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"agent","version":"0.0.0"}}}
{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}
```

Navigate / inspect / interact:

```json
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"navigate_page","arguments":{"type":"url","url":"https://www.google.com/","timeout":20000}}}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"take_snapshot","arguments":{}}}
{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"fill","arguments":{"uid":"u:abc:2:1_5","value":"example text"}}}
{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"click","arguments":{"uid":"u:abc:2:1_8"}}}
```

## Common Commands

List available MCP tools:

```sh
chrome-devtools mcp list --profile conao3
```

Show MCP help:

```sh
chrome-devtools mcp help
chrome-devtools mcp batch --help
```

Inspect daemon health (version, session count, active session count, session pages, queued MCP requests, recent latency maxima, whether the daemon-attached Chrome endpoint responds):

```sh
chrome-devtools daemon status --profile conao3
# profile=conao3 daemon=ready version=0.6.1 sessions=1 active_sessions=0 pages=2 queued_mcp_requests=0 max_control_latency_ms=0 max_forward_latency_ms=42 diagnostic_window_secs=600 chrome=ready port=46071 respawns=0 pid=... socket=...
```

`respawns=<n>` counts daemon-managed MCP respawns. After a respawn, mint a new session.

Stop the profile daemon (refused while sessions are active; `--force` overrides):

```sh
chrome-devtools daemon stop --profile conao3
```

List the tools with `mcp list`; `mcp direct-list` does the same through the running daemon (and says so) because the daemon holds the profile lock.

Bypass the daemon only for manual fallback debugging (cannot preserve snapshot uids across separate invocations, ignores sessions):

```sh
chrome-devtools mcp direct-call --profile conao3
```

Stop the Chrome instance owned by a profile (refused while the profile daemon is running; `--force` overrides):

```sh
chrome-devtools profile stop --profile conao3
```

## Troubleshooting

- If `mcp call` or `mcp batch` exits with `unknown session`, the session expired (30 min idle) or the daemon restarted. Mint a new id via `session create` and retry.
- If `mcp call` or `mcp batch` exits with `session in use: <id> (held by pid <pid> for <n>s)`, another invocation is currently bound to that id; the pid is the holding `chrome-devtools` process (`session list` shows it as `owner_pid`). Wait for it to finish or mint a separate session for the new agent.
- File-writing tool arguments (`take_screenshot` `filePath` etc.) accept paths under your home directory and the OS tempdir.
- `upload_file` falls back to Chrome DevTools Protocol when MCP cannot trigger the file chooser; check daemon logs for `upload_file_fallback` or `upload_file_fallback_failed`.
- If the CLI warns about a daemon version mismatch, the daemon predates the installed CLI. Behavior fixes in the daemon only apply after it restarts; `daemon stop` (without `--force`) is safe once `sessions=0`.
- If `daemon status` shows `respawns` increased, mint a new session before continuing browser work.
- The daemon runs `chrome-devtools-mcp` pinned to 1.8.0 and starts it with `--pageIdRouting`; set `CHROME_DEVTOOLS_MCP_VERSION` (e.g. `latest`) before `daemon start` to override. Up to 1.7.0 that flag is named `--experimentalPageIdRouting`, so those versions log `Unknown arguments` and fall back to their own default for page id routing.
- The daemon-owned `chrome-devtools-mcp` Node heap defaults to 2048 MB; set `CHROME_DEVTOOLS_MCP_MAX_OLD_SPACE_MB` before `daemon start` for heavier trace runs.
- If `fill` or `click` fails with a stale or foreign uid token, take a fresh snapshot in the same session and use the new token.
- If the page is not the expected page, run an `evaluate_script` step that returns `window.location.href` and verify before acting.
- If the profile's Chrome is not running, `mcp call` / `mcp batch` / `mcp list` will start it on first use.
- When Chrome exits unexpectedly, read `~/.cache/chrome-devtools/daemons/<profile>.chrome.log.1` (the previous Chrome's output) for the `FATAL` line.
- If login state is missing, verify the profile name and `user_data_dir`; login cookies are profile-specific.
- If `mcp batch` reports `unknown step type`, check the `type` field; only `tool` and `sleep_ms` are supported.