nodriver — undetected browser automation
PYPI · NODRIVER-MCP · SCANNED SEP 21
Undetected anti-bot Chrome automation. 65 tools, parallel isolated browsers.
Available components
How this component scores in each security and reliability category. Every signal is checked automatically from public evidence about the published package, including repeated runs of it in an isolated sandbox, and we only credit what we can confirm. How we score → Why this is hard to score →
Supply Chain Security50
- Malware scan not yet available for this package.Unverified
- No known CVEs affecting this package version or its production dependencies.Pass
- Runs setuptools.build_meta at install time, a recognised native-build step with no shell scripting around it. View diagnostics → Pass
- 1 of 37 dependencies flagged as unhealthy. View diagnostics → Partial
Provenance & Transparency32
- Source repository is publicly reachable at the declared URL. View diagnostics → Pass
- Provenance check failed: no build-provenance attestation is published. See how to fix → View diagnostics → Fail
- License check failed: no license is declared. See how to fix → Fail
- Actively maintained (last published 26 days ago).Pass
- Disclosure check failed: no security disclosure policy was found in the source repository. See how to fix → Fail
Schema Quality & AI Usability67
- AI-judged instruction clarity (excellent).Pass
- Context-footprint check failed: tool/resource definitions use about 17117 tokens (~263/item across 65 items; 65 tools + 0 resources), over budget; trim descriptions and params. See how to fix → Fail
- Usage-examples check failed: none of the tools include examples. See how to fix → Fail
Stability & Change Management97
- Stability observed for 29 of 30 days with no destabilising changes; credit accrues until the full window elapses.Partial
Tool Coverage100
- 100% of tools have a non-trivial description (not blank, and not just the tool's name).Pass
- 100% of tool parameters carry a description.Pass
- Structured output schemas are declared (97% of tools); any adoption earns full credit.Pass
Tool Safety100
- No prompt-injection markers were found in the server instructions, tool names or descriptions we captured.Pass
- All 5 tool(s) whose name or description implies an irreversible operation declare an MCP destructiveHint annotation.Pass
- An AI judge read all 66 captured unit(s) of tool text and found none that tries to manipulate the model reading it.Pass
Capabilities100
- Implements a supported MCP spec version (2025-11-25); the latest is 2026-07-28.Pass
How do I install the nodriver — undetected browser automation MCP server?
nodriver — undetected browser automation runs locally as a PyPI package, launched with uvx nodriver-mcp. Ready-made configuration for Claude, Cursor, VS Code, Codex and 5 more is on this page, copied from each client's own documentation.
pypi · nodriver-mcp
claude mcp add andresolbach-nodriver-mcp-server -- uvx nodriver-mcp
{
"mcpServers": {
"andresolbach-nodriver-mcp-server": {
"command": "uvx",
"args": [
"nodriver-mcp"
]
}
}
} {
"servers": {
"andresolbach-nodriver-mcp-server": {
"command": "uvx",
"args": [
"nodriver-mcp"
]
}
}
} codex mcp add andresolbach-nodriver-mcp-server -- uvx nodriver-mcp
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"andresolbach-nodriver-mcp-server": {
"type": "local",
"command": [
"uvx",
"nodriver-mcp"
],
"enabled": true
}
}
} openclaw mcp add andresolbach-nodriver-mcp-server --command uvx --arg nodriver-mcp
mcp_servers:
andresolbach-nodriver-mcp-server:
command: "uvx"
args: ["nodriver-mcp"] {
"McpServers": {
"andresolbach-nodriver-mcp-server": {
"Transport": "stdio",
"Command": "uvx",
"Arguments": [
"nodriver-mcp"
]
}
}
} assistant mcp add andresolbach-nodriver-mcp-server -t stdio -c uvx -a nodriver-mcp
{
"mcpServers": {
"andresolbach-nodriver-mcp-server": {
"command": "uvx",
"args": [
"nodriver-mcp"
]
}
}
} Every change we have recorded for this component, newest first. Security-relevant changes are always shown. ▲ marks a change for the better, ▼ a change for the worse; unmarked changes are neutral.
- 21 Sept 26 +1
No change was recorded against any check on this day. Stability & Change Management went from 93 to 97. That category is still filling its 30-day observation window: 28 days of observed history at the previous scan, 29 at this one. The score rises as the window fills, whether or not the server changes.
- 18 Sept 26 +1
No change was recorded against any check on this day. Stability & Change Management went from 83 to 87. That category is still filling its 30-day observation window: 25 days of observed history at the previous scan, 26 at this one. The score rises as the window fills, whether or not the server changes.
- 16 Sept 26 −2
- Stability: pass → 0.80 functional
- 15 Sept 26 0
- Stability: 0.97 → pass security
- 14 Sept 26 −14
- Malware scan: pass → unverified ▼ security
- 13 Sept 26 +15
- Malware scan: unverified → pass ▲ security
- 11 Sept 26 −14
- Malware scan: pass → unverified ▼ security
- 9 Sept 26 +1
- Security disclosure: unverified → fail ▼ functional
Diagnostic detail from the automated scan of this channel: what the scanner observed at each step, so you can see exactly where a check passed or failed. It is informational only and never changes the trust score.
Captured 21 Sept 2026 · Analysed pypi/nodriver-mcp@2.4.0
Provenance No attestation
The registry publishes no build provenance for this version, so there is nothing to verify.
| Result | No attestation |
|---|---|
| Ecosystem | pypi |
Background: How many MCP packages publish verified provenance →
Install scripts 1 script
| Hook | Tier | Command |
|---|---|---|
| build_backend | allowlisted | setuptools.build_meta |
Background: Why install scripts are a supply-chain risk →
Dependencies 37 packages
| Packages resolved | 37 |
|---|---|
| Stale | 1 |
| Tree resolution | Complete |
Background: SBOMs and build attestations, explained →
The tools this component advertises to a client, with an estimated token cost for each. Expand a tool to see its parameters and schema. The per-tool counts are indicative and are not scored directly; the schema's total context footprint is one signal in Schema Quality & AI Usability. A tool's description is untrusted text the model reads on every call, which is what makes this list a security surface and not just an inventory: how tool poisoning works →
set_checked Check or uncheck a box ~343
Tick or untick a checkbox, or select a radio button, and verify it took. Use this instead of fill for anything with a checked state. fill types over a field's text, which a checkbox does not have — and its read-back compares `value`, which a checkbox never changes, so a fill could report success while nothing was selected. Idempotent: asking for the state it is already in is reported and does not click. Otherwise it delivers a real CDP click, on the element or, when the input is visually hidden behind a styled label (the usual custom-control pattern), on the label that controls it. The state is read back afterwards, so a failure is an error rather than a success you cannot trust.
| Name | Type | Req | Description |
|---|---|---|---|
| browser | string | – | Which browser to act on. Each name is an independent Chrome, created on first use. Omit for the shared default; parallel agents each need their own name. |
| checked | boolean | – | True to tick the box or select the radio, False to untick it. Radio buttons cannot be unticked by the browser — select a different one in the group instead. |
| include_snapshot | boolean | – | Append a fresh page snapshot to the response. Worth it when this action changes the page and take_snapshot would be your next call anyway — it saves a round trip, at the cost of a much larger respons… |
| uid | string | yes | Element uid from the most recent take_snapshot, e.g. "4_12". uids are invalidated whenever the page changes — if you get "unknown uid", take a fresh snapshot and retry with the new uid. |
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
set_cookie Set cookie ~215
Set a single browser cookie. Useful for injecting a known session token, consent flag or A/B bucket without walking through a login flow. This creates a session cookie: no expiry, gone when the browser closes. To restore a full cookie set including expiry and SameSite, use load_session. Cookies are browser-wide, not per tab.
| Name | Type | Req | Description |
|---|---|---|---|
| browser | string | – | Which browser to act on. Each name is an independent Chrome, created on first use. Omit for the shared default; parallel agents each need their own name. |
| domain | string | yes | Domain the cookie belongs to, e.g. "example.com". A leading dot (".example.com") makes it apply to subdomains as well. |
| name | string | yes | Cookie name. |
| path | string | – | URL path the cookie is scoped to. "/" covers the whole site. |
| secure | boolean | – | Send the cookie over HTTPS only. Browsers require this for any cookie with SameSite=None. |
| value | string | yes | Cookie value. |
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
set_local_storage Set localStorage ~170
Write entries into the current page's localStorage. Merges into what is already there rather than clearing it. Handy for setting feature flags, consent state or auth tokens before the page's own scripts read them. localStorage is scoped per origin, so navigate to the site first — writing on about:blank goes nowhere. Most pages read these values only at startup, so reload after setting them.
| Name | Type | Req | Description |
|---|---|---|---|
| browser | string | – | Which browser to act on. Each name is an independent Chrome, created on first use. Omit for the shared default; parallel agents each need their own name. |
| items | object | yes | Key-value pairs to write, e.g. {"token": "abc", "locale": "de"}. Values must be strings — JSON-encode anything structured yourself. |
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
set_proxy Route traffic through a proxy ~395
Send this browser's traffic through a proxy, with credentials if it wants them. The proxy itself is a launch flag, so it needs a browser restart — which on an ephemeral profile also drops its cookies. Authentication is handled live: an authenticating proxy stops Chrome at a native dialog that no page and no other CDP command can dismiss, so the page just never loads and nothing says why. Answering it means enabling the Fetch domain, which pauses every request for a round trip, so that only happens when a username is actually set. A proxy that does not ask for credentials costs nothing extra. Call with no arguments to see what is configured. The password is never echoed back. Values set here override the NODRIVER_PROXY environment variable.
| Name | Type | Req | Description |
|---|---|---|---|
| browser | string | – | Which browser to act on. Each name is an independent Chrome, created on first use. Omit for the shared default; parallel agents each need their own name. |
| bypass | string | – | Hosts to reach directly, Chrome's own syntax, e.g. "localhost;127.0.0.1;*.internal". Empty string sends everything through the proxy. |
| password | string | – | Password that goes with `username`. |
| restart | boolean | – | Restart Chrome now so the proxy takes effect, closing all open pages. Default true; false defers it to the next browser start. |
| server | – | – | Proxy to route through, e.g. "http://198.51.100.7:8080" or "socks5://198.51.100.7:1080". A bare "host:port" is treated as http. Pass "" to remove the proxy; omit it to leave the current one alone. |
| username | string | – | Username, for a proxy that demands authentication. Chrome ignores credentials embedded in the proxy URL, so they have to be given here. |
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
shutdown_browser Shut down a browser ~151
Quit one browser's Chrome and free its name, releasing all memory behind it. Use it when an agent is finished with its browser. The difference to close_browser: close_browser quits Chrome but keeps the browser, so its selected profile and runtime flags survive and the next call relaunches Chrome with them. shutdown_browser discards that too, and the name starts fresh if it is ever used again. The "default" browser cannot be retired, since it is the server itself; shutting it down quits its Chrome and resets it. A browser you attached to with use_running_browser is only detached from, never closed: it belongs to the user.
| Name | Type | Req | Description |
|---|---|---|---|
| browser | string | – | Name of the browser to shut down. |
No output schema declared.
No examples provided.
take_memory_snapshot Take heap snapshot ~159
Capture a V8 heap snapshot of the page, for memory-leak debugging. Writes the raw .heapsnapshot file for Chrome DevTools -> Memory -> Load. The usual workflow is two snapshots taken around a suspect interaction, then comparing retained objects between them. On a heavy page a snapshot can be hundreds of megabytes and take several seconds; the response reports the resulting file size.
| Name | Type | Req | Description |
|---|---|---|---|
| browser | string | – | Which browser to act on. Each name is an independent Chrome, created on first use. Omit for the shared default; parallel agents each need their own name. |
| file_path | string | yes | Local path to write the .heapsnapshot file to. Open it in DevTools -> Memory -> Load profile. |
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
take_screenshot Take screenshot ~337
Capture the page, the viewport or a single element as an image. Do NOT use this to read a page. take_snapshot gives you the same content as searchable text, is dramatically smaller, and yields the uids every interaction tool needs. Use a screenshot only when you genuinely need pixels: layout and styling checks, visual regression, or text that exists only inside an image. Element capture needs a uid from the current snapshot. Without file_path the image is returned inline as a base64 data URL.
| Name | Type | Req | Description |
|---|---|---|---|
| browser | string | – | Which browser to act on. Each name is an independent Chrome, created on first use. Omit for the shared default; parallel agents each need their own name. |
| file_path | string | – | Write the image to this local path. Omit to get a base64 data URL inline in the response — that is expensive, so prefer a file for full-page or high-DPR captures. |
| format | string | – | Image format. "png" is lossless and the default; "jpeg" or "webp" with a quality setting are far smaller, which matters for full-page captures. |
| full_page | boolean | – | Capture the whole scrollable page rather than just the visible viewport. Cannot be combined with `uid`. |
| quality | integer | – | Compression quality from 1 to 100, for jpeg and webp only. Ignored for png. 0 leaves Chrome's default. |
| uid | string | – | Capture just this element, using a uid from the most recent take_snapshot. Cannot be combined with full_page. Empty string captures the page. |
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
take_snapshot Take page snapshot ~457
Read the page as compact text, with a uid for every element. This is the primary way to see a page, and the source of the uids that click, fill, hover, drag and upload_file all take. Prefer it over take_screenshot for anything but a genuine visual check: it is searchable, far smaller, and it is what makes interaction possible at all. The output is the accessibility tree — roles, names, values and states, indented by nesting — with Chrome-internal and purely presentational nodes filtered out unless `verbose` is set. A URL on the same origin as the page is printed relative to it — the root node carries the absolute one, so nothing is lost and a link-dense page costs noticeably less to read. uids stay stable across snapshots for elements that did not change, but any page change can invalidate them. Whenever a tool reports "unknown uid", take a fresh snapshot and use the new uid. Output is capped at 200 000 characters. Frames are included and marked, so an element inside an iframe gets a uid like any other and click, fill and evaluate_script work on it unchanged. For plain page text without uids, get_page_content is cheaper; to find elements by CSS selector, use query_selector.
| Name | Type | Req | Description |
|---|---|---|---|
| browser | string | – | Which browser to act on. Each name is an independent Chrome, created on first use. Omit for the shared default; parallel agents each need their own name. |
| file_path | string | – | Write the snapshot to this local path instead of returning it — useful for very large pages you intend to search rather than read in full. |
| include_frames | boolean | – | Also read the page's iframes and splice each one in under the element that hosts it. On by default, because Chrome's accessibility tree stops at a frame boundary and anything inside an iframe would o… |
| verbose | boolean | – | Return the complete accessibility tree, including nodes normally filtered out as noise and containers that are otherwise collapsed. Much larger — use it only when an element you need is missing from… |
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
type_text Type text ~196
Type text into whatever element currently has focus. Appends at the caret instead of replacing, and needs something focused already — click the field first, or use fill, which takes a uid, clears the field and needs no separate focus step. Prefer fill for ordinary form filling. Use type_text when you must add to existing content, or for widgets that only react to raw key events such as contenteditable, rich-text and canvas editors.
| Name | Type | Req | Description |
|---|---|---|---|
| browser | string | – | Which browser to act on. Each name is an independent Chrome, created on first use. Omit for the shared default; parallel agents each need their own name. |
| submit_key | string | – | Key to press once typing finishes, e.g. "Enter" to submit a search box or "Tab" to move to the next field. Empty string presses nothing. |
| text | string | yes | The text to type, one character at a time. |
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
upload_file Upload file ~318
Attach a local file to a file input on the page. Sets the input's files directly over CDP, so no OS file-picker dialog ever opens — clicking an upload button normally would open one, and that blocks every further tool call until a human dismisses it. Chrome renders a file input with an internal shadow button, and that is what the accessibility tree exposes, so the uid you can see is usually not the input itself. This resolves to the real input: the element, one inside it, the input a label controls, or one just above in the tree. The result is read back. If the page's own uploader consumed the file straight away, the response says so, which is a successful upload rather than an empty input.
| Name | Type | Req | Description |
|---|---|---|---|
| browser | string | – | Which browser to act on. Each name is an independent Chrome, created on first use. Omit for the shared default; parallel agents each need their own name. |
| file_path | string | yes | Absolute path to a local file on the machine running this server. |
| include_snapshot | boolean | – | Append a fresh page snapshot to the response. Worth it when this action changes the page and take_snapshot would be your next call anyway — it saves a round trip, at the cost of a much larger respons… |
| uid | string | yes | uid of the file input, or of the button, label or drop zone in front of it — the real <input type="file"> is resolved from there. Comes from the most recent take_snapshot. |
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
use_profile Use persistent profile ~172
Switch the browser to a named persistent profile. Use it to pick up the cookies, logins and extensions stored in an earlier session, so a run starts out already authenticated. Restarts the browser and closes every open page. The profile has to exist already — create it first with create_profile. Only one browser may use a profile at a time; a second instance pointed at the same one fails to start.
| Name | Type | Req | Description |
|---|---|---|---|
| browser | string | – | Which browser to act on. Each name is an independent Chrome, created on first use. Omit for the shared default; parallel agents each need their own name. |
| name | string | yes | Name of an existing persistent profile, as shown by list_profiles. Pass "", "temp", "ephemeral" or "none" to switch back to an ephemeral profile. |
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
use_running_browser Attach to a running browser ~439
Drive a Chrome that is already running, instead of launching a new one. This is how you use a browser you are already signed into. Chrome locks its user-data-dir, so the profile holding your real logins cannot be opened a second time — attaching to the running instance is the only way in, and it saves rebuilding every login through the automation. The user starts Chrome themselves, once: chrome --remote-debugging-port=9222 --user-data-dir=/path/to/profile then this tool attaches to it. Every tool then acts on that browser and its real tabs. `--user-data-dir` is not optional, and it must not be Chrome's own default directory. Since Chrome 136 the port is refused there — Chrome starts normally, says nothing, and nothing ever listens, which is a confusing way to fail. So the everyday profile cannot be reached by adding the flag to it; it has to be a directory of its own, and a copy of an existing profile is accepted (`--profile-directory` then picks which profile inside it). SECURITY: that profile becomes part of the agent's reach. Whatever it is signed into — mail, bank, company systems — is reachable from here, because a cookie jar is all or nothing. Point this at a profile you are willing to expose, not your everyday one. While attached, close_browser and every profile switch only detach; they never close a browser they did not start. Return to a self-launched browser with use_temp_profile or use_profile.
| Name | Type | Req | Description |
|---|---|---|---|
| browser | string | – | Which browser to act on. Each name is an independent Chrome, created on first use. Omit for the shared default; parallel agents each need their own name. |
| host | string | – | Host the browser is listening on. Leave at 127.0.0.1 unless you are attaching to a browser on another machine or in a container. |
| port | integer | – | The remote debugging port Chrome was started with, e.g. 9222 for --remote-debugging-port=9222. |
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
use_temp_profile Use temporary profile ~133
Switch back to a fresh ephemeral profile, created and deleted per session. This is the default. It leaves nothing behind on disk and lets many nodriver instances run concurrently without colliding on a profile directory. Restarts the browser and closes all open pages. The profile you are leaving is not deleted — a persistent one keeps its cookies for the next use_profile. Anything held in the outgoing temp profile is gone.
| Name | Type | Req | Description |
|---|---|---|---|
| browser | string | – | Which browser to act on. Each name is an independent Chrome, created on first use. Omit for the shared default; parallel agents each need their own name. |
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
wait_for Wait for text ~223
Wait until one of several texts appears on the page, then snapshot it. The right way to wait after an action that starts loading — far more reliable than guessing a delay, and it returns the moment the text shows up instead of always burning the whole timeout. On success the page snapshot is included in the response, so no separate take_snapshot call is needed. Polls the visible text twice a second. To wait for an element rather than for wording, use wait_for_selector.
| Name | Type | Req | Description |
|---|---|---|---|
| browser | string | – | Which browser to act on. Each name is an independent Chrome, created on first use. Omit for the shared default; parallel agents each need their own name. |
| text | array | yes | Texts to wait for — the wait ends as soon as any one of them appears. Passing both outcomes, e.g. ["Welcome back", "Login failed"], lets one call tell you which happened. Matching is a case-sensitive… |
| timeout | integer | – | Maximum wait in milliseconds before giving up. |
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
wait_for_selector Wait for element ~207
Wait until an element matching a CSS selector appears on the page. The structural counterpart to wait_for: use it when you know the markup but not the wording, or when the wording is localised. Polls about three times a second and returns as soon as the element shows up. Unlike wait_for it does not return a snapshot, so follow with take_snapshot when you intend to interact.
| Name | Type | Req | Description |
|---|---|---|---|
| browser | string | – | Which browser to act on. Each name is an independent Chrome, created on first use. Omit for the shared default; parallel agents each need their own name. |
| selector | string | yes | CSS selector to wait for, e.g. "#login" or ".results .item". |
| timeout | integer | – | Maximum wait in milliseconds before giving up. |
| visible | boolean | – | Also require the element to have a non-zero size. Use this with frameworks that insert an element into the DOM before rendering it — presence alone would otherwise resolve too early. |
| Name | Type | Req | Description |
|---|---|---|---|
| result | string | yes | – |
No examples provided.
What is the nodriver — undetected browser automation MCP server?
nodriver — undetected browser automation is an MCP server listed in the public MCP registry as io.github.andresolbach/nodriver-mcp-server. Undetected anti-bot Chrome automation. 65 tools, parallel isolated browsers. This page covers its PyPI package (nodriver-mcp).
Is the nodriver — undetected browser automation MCP server safe to use?
nodriver — undetected browser automation scores 65 out of 100 on VerifyMCP. We found no known CVEs affecting it as of 21 September 2026. That is a record of what we were able to check automatically, not an endorsement. The category breakdown on this page shows every signal behind the number, including the ones we could not confirm.
What tools does the nodriver — undetected browser automation MCP server expose?
nodriver — undetected browser automation exposes 65 tools: list_frames, bypass_insecure_warning, cf_verify, click, click_at, and 60 more. Their descriptions and schemas cost roughly 16,297 tokens of context every time the server is loaded.
Is the nodriver — undetected browser automation MCP server still maintained?
nodriver — undetected browser automation is still listed as active in the MCP registry. We last reached this channel on 21 September 2026. Those dates come from our own scans of the registry and the channel itself, not from anything the publisher announced.