A side-by-side look at the Chrome DevTools MCP and Nodriver — undetected browser automation MCP servers: transport, auth, tools, quality signals, and the config to install either one in Claude, Cursor, Windsurf or VS Code.
Verdict: choose Chrome DevTools MCP for browser automation work as a local process; choose Nodriver — undetected browser automation for browser automation as a local process. You can also run both side by side in one mcpServers block.
CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS, CHROME_DEVTOOLS_MCP_NO_UPDATE_CHECKS.click, drag, fill.list_frames, bypass_insecure_warning, cf_verify.Rows where the two servers differ are marked.
| Spec | ||
|---|---|---|
| Category | Browser Automation | Browser Automation |
| Quality signal | 53/100 · Good | 61/100 · Good |
| Install command | Confirmed | Confirmed |
| Transport | Local subprocess (stdio) | Local subprocess (stdio) |
| Auth | Not declared | Not declared |
| Pricing | Not declared | Not declared |
| Required env vars | CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICSCHROME_DEVTOOLS_MCP_NO_UPDATE_CHECKS | None listed |
| Verified owner | Community listing | Community listing |
| GitHub stars | 0 | 6 |
| Directory activity | 1 views 0 installs 0 upvotes | 4 views 0 installs 0 upvotes |
| Tools | 51 | 70 |
| Full listing | View Chrome DevTools MCP | View Nodriver — undetected browser automation |
clickdragfillfill_formhandle_dialoghoverpress_keytype_textupload_fileclick_atclose_pagelist_pageslist_framesList the page's frames, so their content can be reached at all.
Chrome's accessibility tree stops at a frame boundary, and
document.querySelector never crosses one, so anything inside an iframe —
payment fields, consent walls, embedded editors, CAPTCHA widgets — is
invisible to take_snapshot and query_selector by default.
Index 0 is always the main frame. Pass an index or frame id as the `frame`
argument of get_page_content, query_selector, evaluate_script,
wait_for_selector or take_snapshot to work inside one.bypass_insecure_warningClick through Chrome's "Your connection is not private" interstitial.
Use this when a navigation lands on an SSL/certificate warning page (expired
or self-signed certificate, hostname mismatch) instead of the site itself —
the snapshot then shows a warning page rather than the expected content.
This performs the Advanced -> Proceed click for you.
Has no effect on any other kind of page.cf_verifyAttempt to solve a Cloudflare "Verify you are human" challenge.
Use when a page is stuck on a Cloudflare interstitial — a checkbox widget,
or "Checking your browser before accessing". Drives nodriver's built-in
verification bypass, which locates the checkbox visually and clicks it.
Needs opencv, which is an optional extra: install nodriver-mcp[cf], or this
reports that it is missing rather than failing obscurely. Many challenges
clear by themselves after a few seconds, so wait_for(["some text from the
real page"]) is worth trying first — and this says so when it looks at the
page and finds no challenge at all.clickClick an element addressed by its snapshot uid.
This is the click you want in almost all cases — prefer it over click_at,
because a uid survives layout shifts and raw coordinates do not. The element
is scrolled into view first, so it need not be visible beforehand.
Sends real CDP input events, so the page sees `isTrusted=true`, which is the
entire point of an undetected driver. Because those are delivered by
coordinate, the click is aimed at a point that actually hits the element:
several points inside it are hit-tested first, since a sticky header or a
banner can cover the centre. If none of them reach it, `if_covered` decides
what happens, and the response always says which path was taken.
Two other situations force the scripted fallback regardless: a touch-emulated
target, where CDP mouse input can crash the renderer, and a CDP click that
times out or errors. Every step is bounded at 10s, so a wedged page cannot
hang the call.
On "unknown uid", take a fresh take_snapshot and retry with the new uid.click_atClick a raw viewport coordinate instead of an element.
Use `click` with a uid whenever the target shows up in a snapshot — it is
robust against layout shifts, this is not. Coordinates are for surfaces with
no addressable element: canvases, maps, video players, image hotspots.
The point must lie inside the current viewport; scroll_page or
scroll_to_selector first if it does not. Coordinates are in CSS pixels and
ignore the device pixel ratio, so they match what emulate/resize_page report.
Same input path as `click`: real CDP events, the same reported scripted
fallback, the same 10s bound per step.close_pageClose a single browser tab.
The last remaining tab cannot be closed — use close_browser to shut Chrome
down entirely. Closing the selected page clears the selection, so subsequent
tools fall back to the most recently opened tab.
Returns the remaining open pages, because indices shift when a tab closes:
re-read them from this response rather than reusing older ones.close_browserQuit Chrome entirely, closing every tab.
Unlike close_page (which always keeps one tab alive), this tears down the
whole browser. Chrome relaunches automatically on the next tool call with
the currently selected profile, so this is also how you apply pending launch
flags without switching profiles.
On an ephemeral temp profile (the default) this discards cookies, logins and
localStorage — save_session first if you need them back. Persistent profiles
created with create_profile keep everything.
It keeps the browser itself. The name, its selected profile and its launch
flags survive for the next relaunch, so the name goes on counting against the
12-browser limit. When an agent is finished for good, call shutdown_browser
instead: it also releases the name.dragDrag one element onto another (press, move, release).
Both uids must come from the same take_snapshot. Both ends are scrolled into
view and hit-tested first, and the pointer is moved in steps with the button
held, which is what mouse-driven sortables and sliders listen for.
This drives the mouse. Native HTML5 drag-and-drop — the dataTransfer kind —
is a separate protocol that a synthetic mouse does not trigger in Chrome; if
nothing happens on a page that uses it, that is why.emulateEmulate network, CPU, geolocation, user agent, color scheme or viewport.
Applies to the selected page and persists across navigations until
reset_emulation. Every parameter is independent — pass only what you want to
change, leave the rest at their defaults. Settings already in force are
carried over rather than dropped: a user_agent on its own keeps the client
hints a device preset established, and a viewport on its own keeps its touch
and mobile flags unless you clear them with `notouch` / `nomobile`.
To emulate a real phone or tablet, use emulate_device instead: it sets user
agent, client hints, viewport, DPR and touch as one coherent set, which
hand-assembled overrides here get wrong in ways anti-bot systems detect.
Turning `touch` on in the viewport also changes how `click` behaves — CDP
mouse input can crash a touch-emulated renderer, so clicks fall back to the
scripted path (`isTrusted=false`) for as long as touch is enabled.reset_emulationClear every emulation override on the selected page.
Resets in one call: network throttling, CPU throttling, geolocation, user
agent and client hints, color scheme, viewport, device pixel ratio, page
scale and touch emulation — back to the real browser defaults.
Use after emulate or emulate_device to make the page behave like ordinary
desktop Chrome again. Turning touch emulation off here also restores trusted
CDP clicks.emulate_deviceEmulate a phone or tablet with one internally consistent set of signals.
Preferred over assembling `emulate` parameters by hand for mobile work: user
agent, UA client hints (Sec-CH-UA-*), viewport, device pixel ratio, touch
support and Accept-Language are set together so they cannot contradict each
other — a mismatch between them is a classic automation tell.
Presets are pixel_7, pixel_7_landscape and ipad_air (aliases are listed in
the `device` parameter). ipad_air deliberately reports desktop-class Safari
with touch and sends no client hints, which is what a real iPad does.
Applies to the selected page and survives navigation. To have mobile signals
present on a page's very first request, pass `device` to new_page or
navigate_page instead of calling this afterwards. Undo with reset_emulation.evaluate_scriptRun JavaScript inside the page and get the result back as JSON.
The escape hatch for anything the other tools do not cover: reading computed
styles, calling a page's own JS API, or extracting structured data in a
single round trip instead of a dozen snapshot-and-click cycles.
Without `args` the function runs at page level in the main world. With
`args`, the given uids become real element references, which is how you
operate on one specific element from a snapshot.
Return values must be JSON-serialisable — DOM nodes, functions and circular
structures are not, so map them to plain values inside the function. A script
that throws comes back as a failed call carrying the JavaScript error, not as
a result you have to inspect for the word "Error".
Pass script_path to run a function kept in a .js file, and file_path to write
the result to disk instead of into the conversation.Paste either block (or both) into your client config, e.g. claude_desktop_config.json or ~/.cursor/mcp.json. We only show a config when the install command is confirmed.
{
"mcpServers": {
"chrome-devtools-mcp-2": {
"command": "npx",
"args": [
"-y",
"chrome-devtools-mcp@latest"
],
"env": {
"CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS": "YOUR_CHROME_DEVTOOLS_MCP_NO_USAGE_STATISTICS_HERE",
"CHROME_DEVTOOLS_MCP_NO_UPDATE_CHECKS": "YOUR_CHROME_DEVTOOLS_MCP_NO_UPDATE_CHECKS_HERE"
}
}
}
}
{
"mcpServers": {
"nodriver-undetected-browser-automation": {
"command": "uvx",
"args": [
"nodriver-mcp"
]
}
}
}