seleniumbase-mcp¶
The SeleniumBase MCP server provides stealthy browser automation over the Model Context Protocol for MCP clients.¶
This server, (located in server.py), uses SeleniumBase's Pure CDP Mode (seleniumbase.sb_cdp.Chrome), where the browser is driven entirely over the Chrome DevTools Protocol, and there is no WebDriver in the loop at all, which makes it SeleniumBase's stealthiest mode. CAPTCHA-solving is included via the solve_captcha() method!
Other SeleniumBase automation styles, (such as Driver() and SB()), have their own MCP servers in seleniumbase/seleniumbase-mcp.
headless defaults to None in start_browser, which resolves to headless on Linux (typical for server/container environments) and headed on Windows/macOS. Pass headless=True or headless=False explicitly to override this for any OS; headless mode may be less stealthy.
1. Install¶
There are two ways to get the seleniumbase-mcp command:
If you just want to use the server (simplest — no repo clone needed):
pip install "seleniumbase[mcp]"
This installs seleniumbase from PyPI along with the mcp[cli] extra, and registers a seleniumbase-mcp console-script command. Your MCP client config can be as simple as {"command": "seleniumbase-mcp"} (see step 3's Option A).
If you're working from a git clone of this repo (instead of a PyPI install):
(Requires uv)
This folder lives inside the SeleniumBase repo, so if you've already cloned SeleniumBase, just cd into this folder and sync:
cd mcp_servers
uv sync
uv sync reads pyproject.toml, creates a .venv/ in this folder, and installs mcp[cli] plus seleniumbase, which is resolved from the local SeleniumBase checkout one directory up (in editable mode, via [tool.uv.sources] in pyproject.toml), not from PyPI. It also installs this project itself, which registers a seleniumbase-mcp console-script command via [project.scripts], pointing at server.py's main() function (mcp.run(transport="stdio")). That's what lets uv run seleniumbase-mcp work as the MCP client command in steps 3 and 4 below.
Pure CDP Mode doesn't use WebDriver, so no chromedriver download is needed... just a working Chrome/Chromium install.
(No uv? python3 -m venv venv && pip install -r requirements.txt works too. requirements.txt installs the local SeleniumBase checkout via -e . the same way. Substitute python server.py for uv run seleniumbase-mcp everywhere below, and use absolute venv/bin/python + script path in your MCP client config instead of the path-free options.)
2. Try it standalone (optional sanity check)¶
uv run mcp dev server.py
That opens the MCP Inspector, where you can test commands ("Tools"). Ctrl+C to exit. Next step is wiring it into a client.
3. Connect it to Claude Desktop¶
Claude Desktop doesn't run from a "project" directory the way Claude Code does, so a bare uv run seleniumbase-mcp isn't guaranteed to find this folder. Two ways to get a stable config:
Option A — global install (recommended, zero paths anywhere):
uv tool install . # from inside this folder, installs the command globally
This puts seleniumbase-mcp on your PATH permanently (run uv tool ensurepath once if it warns that its bin directory isn't on PATH yet). Then claude_desktop_config.json can be just:
{
"mcpServers": {
"seleniumbase-mcp": { "command": "seleniumbase-mcp" }
}
}
Note this bakes in the location of the SeleniumBase checkout at install time (since seleniumbase resolves to ../ via the editable path source). If you move or delete this clone, re-run uv tool install . from its new location.
Option B — point uv at this folder directly (one absolute path, but no venv/interpreter path to track down, and no separate install step):
{
"mcpServers": {
"seleniumbase-mcp": {
"command": "uv",
"args": ["--directory", "/absolute/path/to/SeleniumBase/mcp_servers", "run", "seleniumbase-mcp"]
}
}
}
The location of claude_desktop_config.json depends on your system:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
Restart Claude Desktop. You should see a 🔨 tools icon indicating the server connected, with the following MCP tools available through the tools interface:
start_browserclose_browsernavigatemanage_historyget_page_infofind_elementsget_contentget_attributescheck_conditionclickhover_actiontype_textselect_optionfocuswait_forassert_conditionmanage_cookiesmanage_storagescrollmanage_windowmanage_tabssolve_captchasave_outputrun_javascript
4. Connect it to Claude Code¶
This folder's .mcp.json is checked in and ready to use as-is.
No path editing is required because uv run seleniumbase-mcp resolves this project from pyproject.toml in the current directory:
{
"mcpServers": {
"seleniumbase-mcp": {
"type": "stdio",
"command": "uv",
"args": ["run", "seleniumbase-mcp"]
}
}
}
The .mcp.json file helps clients connect to the MCP server.
- Claude Code auto-loads
.mcp.jsonfrom whatever directory you launchclaudein. uv run seleniumbase-mcpneedspyproject.tomlto be discoverable from the current directory. That resolves cleanly when.mcp.jsonandpyproject.tomlsit next to each other.
Run claude from inside mcp_servers/ or the root folder to get it auto-loaded.
If you'd rather register it manually instead of relying on .mcp.json:
claude mcp add seleniumbase-mcp -- uv run seleniumbase-mcp
(run from inside this folder, for the same reason as above.)
Selectors¶
Most tools accept a selector argument. Behavior varies slightly by tool, so check a tool's own docstring when it matters:
- CSS selectors are preferred and supported by every tool that takes a selector.
- XPath is accepted by several (not all) tools. Some tools go through SeleniumBase's XPath-to-CSS conversion first; expressions that can't be converted (e.g.
contains(...)) aren't supported by those tools. - SeleniumBase's visible-text selector syntax, e.g.
a:contains("Sign in"), is accepted by several tools (includingclick, when not usingall_matches) but not all of them —find_elements, for example, only supports CSS/XPath.
Tools exposed¶
Tools here are grouped around a shared selector convention. Several near-identical one-off tools (e.g. separate click/hover/drag/wait/cookie/storage variants) have been consolidated into a single tool with a mode/action/state/check parameter, so there are fewer near-neighbor tools to disambiguate between while every underlying capability stays available.
| Group | Tool(s) |
|---|---|
| Session | start_browser(url, headless, use_chromium, browser_executable_path, incognito, guest, ad_block, proxy), close_browser |
| Navigation | navigate, manage_history(action: back/forward/reload/list), get_page_info (running status, url, title, origin, user agent in one call) |
| Finding & reading | find_elements(selector, timeout, include_html), get_content(selector, output_format: text/html/urls, timeout), get_attributes(selector, attribute, timeout), check_condition(check: present/visible, text) |
| Interacting | click(selector, nth, all_matches, only_if_visible, parent_selector, timeout, scroll), hover_action(selector1, selector2, action: none/click/drag_and_drop), type_text(mode: fill_input/append/fast_type/set_value/clear_only), select_option(by: text/value/index), focus(action: scroll_to_element/focus/highlight, timeout) |
| Waiting | wait_for(state: present/visible/not_visible/absent/seconds_passed, text) |
| Assertions | assert_condition(check: element_present/element_visible/text_visible/title/url/url_contains) |
| Cookies & storage | manage_cookies(action: get_all/clear/save/load), manage_storage(storage: local/session, action: get/set) |
| Scrolling | scroll(direction: up/down/top/bottom, amount) |
| Windows & tabs | manage_window(action: get_rect/set_rect/maximize/minimize), manage_tabs(action: list/open/switch/switch_newest/close_active) |
| Captcha | solve_captcha |
| Output & misc | save_output(format: screenshot/html/pdf), run_javascript |
Design notes / things to adapt for your use case¶
-
Single global session. The server holds one browser session at a time. This matches how MCP servers are typically launched (one process per client connection) and keeps the tool surface simple. If you need multiple concurrent browser tabs/sessions, you'd extend this to a dict of named sessions and add a
session_idparameter to each tool. -
Blocking calls. SeleniumBase's calls are synchronous and will block the server while a page loads or an element is waited on. For a single-user local tool this is fine; for a multi-client server you'd want to run them in a thread pool via
asyncio.to_thread. -
start_browserretries once before failing. If the first launch attempt raises, it's retried once automatically before returning an error. This was added after seeing occasional first-attempt failures when testing against Glama's MCP Inspector; it costs nothing on the common case where the first launch already succeeds. -
Two error-handling paths, by design. Most failures (a selector isn't found, an assertion fails, an invalid
action/mode/checkvalue is passed) are caught by thehandle_sb_errorsdecorator and returned as a descriptive string, e.g.Error in click: NoSuchElementException - ..., so the calling agent can read the failure and self-correct. There's one deliberate exception: calling any tool other thanstart_browser/close_browserwhen no browser session is running raisesToolError(via the shared_get_sb()helper) instead of returning a string.handle_sb_errorsexplicitly re-raisesToolErrorrather than catching it, so this surfaces to the MCP client as a real tool-call error (is_error=True), not as ordinary text the agent has to pattern-match on.start_browserandclose_browserhandle their own lifecycle errors directly (e.g. "already running", a failedquit()) and also return strings rather than raising. -
No standalone session-status tool. There is no separate
browser_status-style tool.get_page_infodoubles as the status check: it returns{"running": False}(optionally with anerrorfield) when there's no active session or the session errors out, and page metadata (running: True,url,title,origin,user_agent) otherwise.get_page_infodoes not include navigation history — that lives onmanage_history(action="list")instead (see below). -
Navigation and history live in one tool:
manage_history. What used to benavigate_historyis nowmanage_history, and it gained a fourth action:"list", which returns the browser's navigation history as{"position": <0-indexed current entry>, "entries": [...]}, where each entry hasid,url,user_typed_url,title, andtransition_type."back","forward", and"reload"behave as before. This is the only way to retrieve navigation history now —get_page_infodoesn't return it. -
get_contentalways reads from an element, not the whole document.selectornow defaults to"body"rather thanNone/whole-page, and there's noinclude_shadow_domoption anymore —get_contentno longer callsget_page_sourceat all.output_format="html"returns a single element's outer HTML (get_element_html), andoutput_format="urls"returns URLs discovered within that element (get_all_urls(selector=...)), rather than the full raw page source including shadow roots. If you need the complete page source (shadow DOM included), that capability isn't exposed by any tool here currently.get_contentalso gained atimeoutparameter (default 5s) for waiting on the target element. -
get_attributesandfocusnow take atimeout. Both default to 5 seconds and wait for the target element the same way most other interaction tools do; previously neither exposed a timeout. -
check_conditionis deliberately narrow. Itscheckparameter only accepts"present"or"visible"— there's no built-in"count"check anymore; callfind_elementsand read the returnedcountfield instead. Passingtextchecks whether that text is visible withinselectorand takes priority overcheckwhen both are given — socheck_condition(text="Sign in")behaves differently fromcheck_condition(check="visible"), not as two variants of the same check. Note that an empty string fortext(or forwait_for'sselector/text) is treated as not provided, since both tools now branch on truthiness rather than onis not None. -
find_elementscatches its own lookup failures. Its defaulttimeoutis 0.5 seconds (not 5, unlike most other tools here). A failed or empty lookup never raises: no matches returns{"count": 0, "matches": []}, and an actual lookup error (e.g. an unsupported selector) returns{"count": 0, "matches": [], "error": "<details>"}— the error lives inside the returned dict rather than surfacing as a top-level string fromhandle_sb_errors. Pass a longertimeoutexplicitly if the elements you're looking for may still be loading. -
wait_secondswas folded intowait_for. There's no standalonewait_secondstool anymore. Usewait_for(state="seconds_passed", timeout=<seconds>)instead — it ignoresselector/textand blocks for the fulltimeoutduration. All otherwait_forstates behave as before. -
Hover, click-after-hover, and drag-and-drop share one tool.
hover_action(selector1, selector2, action)replaces the earlier separatehoveranddrag_and_droptools.action="none"hoversselector1only;action="click"hoversselector1then clicksselector2(useful for dropdown/submenu items revealed by hovering);action="drag_and_drop"dragsselector1ontoselector2. (selector2is required whenactionis"click"or"drag_and_drop".) -
Non-activating element actions are
focus. What used to beact_on_elementis nowfocus(selector, action, timeout), with actionsscroll_to_element(the default),focus, andhighlight— note the default action is scrolling the element into view, not focusing it. None of these actions click, type into, select from, or otherwise activate the element; useclick,type_text,select_option, orhover_actionfor that. -
scroll'samountisn't capped at 100. Relative up/down scrolling by more than 100% of the viewport height is allowed (e.g.amount=200scrolls roughly two viewport heights); negative amounts are rejected for"up"/"down". -
Elements don't cross the wire as handles. In native CDP Mode,
find_element()returns a live object with its own methods (el.click(),el.get_html(), ...). MCP tools can only return JSON-serializable data, sofind_elementsresolves each match immediately to a plain dict (tag_name,text, and optionallyhtml) instead of returning a handle you could call further methods on. If you need to act on one of several matches, useclick(selector, nth=...)(acts by position) rather than "find, then click" as two separate steps. -
CAPTCHA-solving.
solve_captchaattempts to detect and interact with several challenge types over CDP (e.g. Cloudflare Turnstile, reCAPTCHA, hCaptcha, DataDome Slider, FriendlyCaptcha), including slider-style drag interactions, without guaranteeing success. -
Security.
run_javascriptruns arbitrary JS, andmanage_storagecan expose authentication/session secrets;manage_cookiesandsave_outputaccept filenames/folders that can touch the filesystem. This server can also drive a real browser to real sites — don't expose it over an untrusted network transport; stdio + local trust (the default here) is the safe setup.
Extending¶
Adding a tool is just adding a @mcp.tool()-decorated function (wrapped in handle_sb_errors) that calls the matching sb_cdp.Chrome method — SeleniumBase has methods for file uploads, network conditions, and more that aren't wrapped above yet.