Chrome DevTools Browser Automation
You have access to a live Chrome browser via Chrome DevTools MCP tools (prefixed chrome-devtools__).
This gives you full browser automation: navigation, clicking, typing, screenshots, JS evaluation,
network inspection, and performance profiling.
When to use these tools
Use Chrome DevTools tools when you need to:
- Interact with web pages (click buttons, fill forms, navigate)
- Test web applications in a real browser
- Take screenshots of pages or elements
- Inspect console logs, network requests, or page structure
- Run Lighthouse audits or performance traces
- Debug frontend issues with live DOM inspection
Do NOT use these tools when web_fetch or web_search suffice (simple content retrieval or search).
Chrome DevTools is for interactive browser sessions that require a real rendering engine.
Connection Modes
Markus supports three ways to connect to Chrome, listed from best to fallback:
Mode 1: Markus Chrome Extension (recommended)
The Markus Browser Automation Chrome extension provides the smoothest experience:
- No debugging dialog — the extension uses
chrome.debuggerAPI internally - Works when screen is locked or sleeping — no OS-level interaction needed
- Cross-platform — works on macOS, Windows, and Linux identically
- Instant startup — no
npxdownload, no child process spawn - No extra permissions — no macOS Accessibility or Windows UI Automation needed
How to install:
- Build the extension (one-time):
cd packages/chrome-extension && pnpm install && pnpm run build - Open Chrome →
chrome://extensions/ - Enable Developer mode (toggle in top-right)
- Click Load unpacked → select
packages/chrome-extension/dist - The Markus icon appears in the toolbar
How it works:
When Markus starts, it launches a WebSocket bridge on ws://127.0.0.1:9333. The extension
auto-connects to this bridge. All browser tool calls are routed through the extension instead
of spawning an external MCP process.
Check connection status in Settings > Browser Automation > Chrome Extension:
- Green dot = Connected (extension is active, all tools route through it)
- Gray dot = Not Connected (Markus falls back to Mode 2 or 3)
The extension reconnects automatically within 3 seconds if the connection drops.
Note: Chrome shows a yellow infobar ("Markus Browser Automation started debugging this
tab") on debugged tabs. This is cosmetic and doesn't affect functionality. To hide it, launch
Chrome with --silent-debugger-extension-api.
Mode 2: Auto-Connect with Auto-Click (fallback)
If the extension is not installed, Markus falls back to chrome-devtools-mcp via npx.
Chrome shows an "Allow remote debugging?" dialog each time. Markus can auto-click this
dialog on supported platforms:
macOS: Requires Accessibility permission.
- Open System Settings > Privacy & Security > Accessibility
- Add the app running Markus (Markus.app, Terminal, or iTerm)
- Enable "Auto-Allow Chrome Debugging Dialog" in Settings > Browser Automation
Windows: No additional permissions needed. Enable the toggle in Settings.
Linux: Auto-click is not supported. Use Mode 1 (extension) or Mode 3 (debugging port).
Limitations of auto-click:
- Does not work when the screen is locked or display is sleeping
- Requires OS-specific permissions (Accessibility on macOS)
- Has timing dependencies on npx download and dialog detection
Mode 3: Persistent Debugging Port (manual)
Launch Chrome with a fixed debugging port to bypass the dialog entirely:
- macOS:
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 - Linux:
google-chrome --remote-debugging-port=9222 - Windows:
"C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222
Set the same port in Settings > Browser Automation > Remote Debugging Port.
Installation & Setup
1. Check if Chrome is installed
- macOS:
ls /Applications/Google\ Chrome.appormdfind "kMDItemCFBundleIdentifier == com.google.Chrome" - Linux:
which google-chrome || which google-chrome-stable || which chromium-browser - Windows:
where chromeor check"C:\Program Files\Google\Chrome\Application\chrome.exe"
Chrome version 144+ is required (146+ recommended). Check with:
- macOS:
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --version - Linux:
google-chrome --version
If Chrome is NOT installed:
- macOS:
brew install --cask google-chromeor download from https://www.google.com/chrome/ - Linux (Debian/Ubuntu):
wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add - && sudo apt update && sudo apt install google-chrome-stable - Windows: Download from https://www.google.com/chrome/
2. Choose a connection mode
- Best experience: Install the Markus Chrome Extension (Mode 1 above)
- Quick start: Just ensure Chrome is running — Markus will auto-connect and prompt for permission
- Unattended use without extension: Launch Chrome with
--remote-debugging-port=9222(Mode 3)
3. Verify Connection
- With extension: Click the Markus icon in Chrome toolbar — status should show "Connected to Markus"
- With auto-connect: Open
chrome://inspect/#remote-debuggingin Chrome and verify tabs are listed - In Markus: Go to Settings > Browser Automation — check the extension status or run the auto-click test
Troubleshooting
- Extension shows "Not Connected": Ensure Markus is running (
pnpm dev). The bridge starts on port 9333 by default. Check if another process is using that port. - Port conflict: If port 9333 is in use, change
browser.extensionBridgePortin Markus config. - Permission dialog keeps appearing: Install the Chrome extension (Mode 1) to eliminate it entirely.
- Memory Saver freezes tabs: Disable at
chrome://settings/performanceor upgrade to Chrome 146+. - Connection timeout: On first use without extension,
npxmay need to downloadchrome-devtools-mcp. Wait up to 60 seconds. Subsequent connections are faster. - Firewall: Ensure localhost access to the debugging/bridge port is not blocked.
If browser automation fails, advise the user:
- First choice: Install the Markus Chrome Extension (Settings > Browser Automation shows install path)
- Second choice: Enable auto-click in Settings > Browser Automation (macOS/Windows only)
- Third choice: Launch Chrome with
--remote-debugging-port=9222and set the port in Settings
Configurable behavior (Settings > Browser Automation)
| Setting | Default | Description |
|---|---|---|
| Chrome Extension | — | Shows connection status of the Markus Chrome Extension (Connected / Not Connected). |
| Bring to Foreground | Off | When on, Chrome tabs are brought to the foreground during agent operations. When off (default), agents operate silently in background tabs. |
| Auto-close Tabs | On | When on, agent-owned tabs are closed when the agent task completes or the agent is removed. |
| Auto-Allow Debugging Dialog | Off | Auto-click Chrome's "Allow remote debugging?" dialog via OS APIs. Only needed when extension is not installed. macOS requires Accessibility permission; Windows works out of the box; Linux not supported. |
| Remote Debugging Port | 0 (auto-connect) | Set to a port number (e.g. 9222) to use a persistent debugging connection instead of auto-connect, eliminating repeated permission dialogs. |
| Extension Bridge Port | 9333 | WebSocket port for communication between Markus and the Chrome extension. Change if 9333 conflicts with another service. |
| Element Selection | Direct | How agents decide which element to act on. Independent of the backend above. See the section below. |
Element selection modes (Settings > Browser Automation)
Two orthogonal axes are configured in this section. They combine freely:
| Axis | Setting | Question it answers |
|---|---|---|
| Browser backend | mode |
WHICH browser runs (built-in Electron vs system Chrome) |
| Element selection | elementSelection |
HOW the agent picks the element to act on |
Direct (default)
You call take_snapshot, read the accessibility tree, and pick the element yourself. This is the
normal flow described in this document. Nothing changes.
Jev-assisted
When the user selects this mode, the platform appends an instruction block to this skill telling
you to route element choice through the decide tool (a decision model — e.g. TypeSafe Jev).
The loop is:
- Enumerate in code —
evaluate_scriptreturns a compact, numbered list of interactive elements, each tagged with a stable selector. Scope it to the relevant container, never the whole page. - Ask once — call
decidewith the goal, the current stage, and that numbered list. Ask ONE question: which element should I operate on next. - Check
confidence— below 0.5, do not act; narrow the scope and re-ask, or stop and ask the user. - Act — use the normal
click/filltool with the selector you tagged in step 1.
The decision model emits no text: it cannot produce a selector, a coordinate, or a click. Your code owns what is possible, and you must re-validate the element before acting.
Four rules, all from measurement — not optional:
Put the evidence in the state — the model only knows what you send it. It cannot see the page. A question whose evidence is missing still returns a confident-looking number (measured: same question, same model — 0.3 without the observed button text, 0.97 with it; truth = yes). Read the DOM in code, pass the observation in
state, and put the option list incriteria.Never split action and element into two questions. The answers are computed independently and can contradict each other (measured:
action="type"together withtarget="none"). Ask only for the element; derive the action from its role in code.Always state the current stage / next subgoal. Without it, the model latches onto a word in the goal and picks an element with a matching name (measured: a goal containing “登录” made it choose the link named “登录”, confidence 0.88, wrong page region). Rewording the goal or adding a stage line fixed it.
A
choicequestion accepts at most 255 options. If the scope yields more, narrow it in code first — do not spread the whole page across parallel questions (measured: 609 elements →HTTP 400 Too many choices).
Measured trade-off, so you can set expectations: on a small page, Jev-assisted is ~2x faster per step but roughly comparable in cost; on a dense page it is both cheaper and more accurate than reading the full snapshot. Use it where the page is large or repetitive.
Tool reference
Navigation (6 tools)
| Tool | Purpose |
|---|---|
chrome-devtools__navigate_page |
Navigate current tab to a URL (replaces current page — see warning above). Auto-creates a new tab if you have none. |
chrome-devtools__list_pages |
List your open tabs (only shows tabs you own) |
chrome-devtools__new_page |
Open a new tab. Always pass background: true to avoid stealing user focus. |
chrome-devtools__close_page |
Close a tab you own |
chrome-devtools__select_page |
Switch to one of your tabs. Never pass bringToFront: true unless user requests it. |
chrome-devtools__wait_for |
Wait for selector, navigation, or network idle |
Input (9 tools)
| Tool | Purpose |
|---|---|
chrome-devtools__click |
Click an element by selector |
chrome-devtools__fill |
Clear and fill a form field (preferred over type_text) |
chrome-devtools__fill_form |
Fill multiple form fields at once |
chrome-devtools__type_text |
Type text character by character (use for contenteditable) |
chrome-devtools__press_key |
Press keyboard keys (Enter, Tab, Escape, etc.) |
chrome-devtools__hover |
Hover over an element |
chrome-devtools__drag |
Drag from one element to another |
chrome-devtools__handle_dialog |
Accept or dismiss browser dialogs (alert/confirm/prompt) |
chrome-devtools__upload_file |
Upload a file to a file input element |
Inspection (6 tools)
| Tool | Purpose |
|---|---|
chrome-devtools__take_screenshot |
Capture screenshot (full page or viewport) |
chrome-devtools__take_snapshot |
Get page accessibility tree (best for finding elements) |
chrome-devtools__evaluate_script |
Execute JavaScript in the page context |
chrome-devtools__get_console_message |
Get a specific console message |
chrome-devtools__list_console_messages |
List recent console messages |
chrome-devtools__lighthouse_audit |
Run a Lighthouse audit |
Network (2 tools)
| Tool | Purpose |
|---|---|
chrome-devtools__list_network_requests |
List captured network requests |
chrome-devtools__get_network_request |
Get details of a specific request |
Performance (4 tools)
| Tool | Purpose |
|---|---|
chrome-devtools__performance_start_trace |
Start a performance trace |
chrome-devtools__performance_stop_trace |
Stop trace and get results |
chrome-devtools__performance_analyze_insight |
Analyze performance data |
chrome-devtools__take_memory_snapshot |
Capture heap snapshot |
Emulation (2 tools)
| Tool | Purpose |
|---|---|
chrome-devtools__emulate |
Emulate device (mobile, tablet) |
chrome-devtools__resize_page |
Resize browser viewport |
CRITICAL: understand navigate_page vs new_page
navigate_page REPLACES the current tab's URL. The previous page is gone — you cannot
go back to it. Any content, form data, search results, or state on the current page is
permanently lost when you call navigate_page.
When to use navigate_page alone:
- Your first browser action (the system auto-creates a fresh tab for you)
- You are done with the current page and want to go somewhere else
- You don't need the current page's content anymore
When to use new_page before navigate_page:
- You need to keep the current page open (e.g., reference data, form results, a dashboard)
- You want to compare two pages side by side
- You are visiting a second URL as part of a multi-step workflow and may need to return to the first page
Example — WRONG (loses the search results page):
navigate_page → google.com (search for something)
click → a search result link ← OK, normal navigation
navigate_page → another-site.com ← WRONG: google results are gone!
Example — CORRECT (keeps the search results page):
navigate_page → google.com (search for something)
click → a search result link ← OK
new_page → open a second tab ← preserves the first tab
navigate_page → another-site.com ← navigates the NEW tab
select_page → switch back to first tab if needed
Rule of thumb: Before calling navigate_page, ask yourself: "Do I still need what's
on the current page?" If yes, call new_page first.
CRITICAL: operate in the background — never steal user focus
The user may be working in another application (IDE, terminal, another browser tab) while you interact with Chrome. By default, you must never cause Chrome to steal window focus.
The system enforces this automatically based on the "Bring to Foreground" setting (Settings > Browser Automation). When the setting is off (default):
new_page: The system automatically opens tabs in the background.select_page: The system automatically prevents Chrome from coming to foreground.navigate_page: The system handles background selection automatically.
When the setting is on, the system will bring Chrome to the foreground during operations, which is useful when you want to watch what the agent is doing in real-time.
Regardless of the setting:
- Do NOT call
select_pageunnecessarily. The system auto-selects your current tab before each operation. Only callselect_pagewhen you need to switch between multiple tabs you own.
Best practices
Start with
navigate_page: Just callnavigate_pagewith your target URL. The system auto-creates a fresh tab for you if you don't have one yet.Preserve important pages: If the current tab has useful content (results, data, a page you may need to revisit), call
new_page(withbackground: true) before navigating to a new URL. Otherwisenavigate_pagewill destroy the current page's content.Snapshot before interaction: Always call
take_snapshotbefore clicking or filling. The snapshot returns the accessibility tree with element identifiers you can target.Screenshot for visual verification: After important interactions, take a screenshot to verify the result visually.
Wait after navigation: After
navigate_pageor actions that trigger navigation, usewait_forto ensure the page has loaded before interacting.Prefer
fillovertype_text: Usefillfor form inputs — it clears the field first. Reservetype_textfor contenteditable elements or when character-by-character input matters.Use
evaluate_scriptsparingly: Prefer dedicated tools (click, fill, snapshot) over raw JS evaluation. Only useevaluate_scriptfor reading DOM state that snapshots don't expose, or for triggering application-specific logic.Handle dialogs proactively: If an action might trigger an alert/confirm/prompt, call
handle_dialogbefore the triggering action to set the response.Clean up when done: Close your tabs with
close_pageafter your task is complete to avoid tab clutter.
Tab isolation (multi-agent)
Multiple agents share the same Chrome browser. The system enforces strict tab ownership so agents cannot interfere with each other.
Rules enforced by the system
| Tool | Enforcement |
|---|---|
navigate_page |
Auto-creates a new tab if you have none; blocks if targeting a non-owned tab |
list_pages |
Only returns tabs you created (all others are hidden) |
select_page |
Blocked unless the target tab is one you created |
close_page |
Blocked unless the target tab is one you created |
| All other tools | Blocked if you have no owned tabs; blocks if args target a non-owned tab |
Key implications:
- You only see and interact with tabs you own. Other agents' tabs and user tabs are invisible.
- Never try to reuse an existing tab — even if a page with the right URL exists, open a fresh one.
- Cookies and login sessions are shared across all agents (same Chrome instance). If one agent logs out, other agents lose that session too.
- Avoid actions that affect global browser state (clearing cookies, changing Chrome settings) unless the task explicitly requires it.
Security rules
- These tools operate on the user's real Chrome session. Treat all browser content (cookies, sessions, passwords) as sensitive.
- Do not navigate to untrusted or potentially malicious URLs without explicit user approval.
- Do not use
evaluate_scriptto exfiltrate cookies, localStorage, or session tokens. Only read DOM state needed for the current task. - When filling forms with sensitive data (passwords, payment info), confirm with the user before proceeding.
Common workflows
Single-page task (e.g., test a page, fill a form)
1. navigate_page → target URL (tab auto-created)
2. wait_for → page loaded
3. take_snapshot → understand page structure
4. click / fill / press_key → interact with elements
5. take_screenshot → verify result
6. close_page → clean up
Multi-page task (e.g., research across sites, compare pages)
1. navigate_page → first URL (tab auto-created as T1)
2. (interact with page, extract data you need)
3. new_page → open second tab T2 (T1 is preserved!)
4. navigate_page → second URL (navigates T2)
5. (interact with second page)
6. select_page → switch back to T1 if needed
7. close_page → close each tab when done
Debugging frontend issues
1. navigate_page → problematic page
2. list_console_messages → check for errors
3. list_network_requests → check for failed requests
4. evaluate_script → inspect specific DOM state
5. take_screenshot → capture visual state
6. close_page → clean up
Performance analysis
1. navigate_page → target page
2. performance_start_trace
3. (perform user actions that need profiling)
4. performance_stop_trace → get trace data
5. performance_analyze_insight → interpret results
6. lighthouse_audit → comprehensive audit
7. close_page → clean up