.vscode — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited .vscode (MCP Server) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Selenium MCP Server
A Model Context Protocol (MCP) server that provides web automation capabilities through Selenium WebDriver. This server allows AI assistants to interact with web pages by providing tools for navigation, element interaction, taking screenshots, and more.
# Install
pip install mcp-server-selenium
# Run
python -m mcp_server_selenium --port 9222 --user_data_dir /tmp/chrome-debug# Clone and setup
git clone https://github.com/PhungXuanAnh/selenium-mcp-server.git
cd selenium-mcp-server
uv sync
# Run
PYTHONPATH=src python -m mcp_server_selenium --port 9222 --user_data_dir /tmp/chrome-debugThe default compact profile is documented in Compact Profile (Default). The explicit --tool-profile legacy compatibility fallback provides the following 24 tools with their original names and call contracts.
navigate(url, timeout) - Navigate to a specified URL with Chrome browsercheck_page_ready(wait_seconds) - Check if the current page is fully loaded with optional waitlist_tabs() - List all browser tabs and identify the active tabopen_tab(url=None) - Open a new tab and optionally navigate it to a URLswitch_tab(handle) - Switch to a tab using a handle returned by list_tabsclose_tab(handle=None) - Close a specific tab, or the active tab when no handle is providedtake_screenshot(file_name, directory="tmp/selenium-screenshot") - Take a screenshot of the active tab. A descriptive file_name is required; .png is added when omitted. When the Agent knows its current workspace path, it should prefer an absolute directory inside that workspace so the destination does not depend on the MCP server cwd. Otherwise, omit directory to use the configured workspace default. Existing files receive a numeric suffix instead of being overwritten.record_video(action, file_name="", directory="tmp/selenium-video", include_address=false, window_handle="", max_duration_seconds=600, window_handles=[]) - start records one fixed window_handle, one MP4 following switches among 2-4 window_handles, or the active tab when both are omitted. The selectors are mutually exclusive. Always call stop immediately after the final browser action; the finite deadline auto-finalizes only as a fallback. Paths follow screenshot rules and include_address=true adds a synthetic URL header.get_an_element(text, class_name, id, attributes, element_type, in_iframe_id, in_iframe_name, return_html, xpath) - Get an element identified by various criteriaget_elements(text, class_name, id, attributes, element_type, in_iframe_id, in_iframe_name, page, page_size, return_html, xpath) - Get multiple elements with pagination supportget_direct_children(text, class_name, id, attributes, element_type, in_iframe_id, in_iframe_name, return_html, xpath, page, page_size) - Get all direct child nodes of an element with paginationclick_to_element(text, class_name, id, attributes, element_type, in_iframe_id, in_iframe_name, element_index, xpath) - Click on an element identified by various criteriaset_value_to_input_element(text, class_name, id, attributes, element_type, input_value, in_iframe_id, in_iframe_name, xpath) - Set a value to an input elementget_style_an_element(text, class_name, id, attributes, element_type, in_iframe_id, in_iframe_name, return_html, xpath, all_styles, computed_style) - Get style information for an elementrun_javascript_in_console(javascript_code) - Execute JavaScript without intentionally reading buffered console logsrun_javascript_and_get_console_output(javascript_code) - Drain old console logs, execute JavaScript, then return its value and newly captured console outputget_console_logs(log_level) - Read and consume browser console logs with optional level filteringget_network_logs(filter_url_by_text, only_errors_log) - Read and consume performance logs as network events with optional filteringget_response(request_id) - Retrieve a response body using a request ID from get_network_logslocal_storage_add(key, string_value, object_value, create_empty_string, create_empty_object) - Add or update a key-value pair in browser's local storagelocal_storage_read(key) - Read a value from browser's local storage by keylocal_storage_read_all() - Read all key-value pairs from browser's local storagelocal_storage_remove(key) - Remove a key-value pair from browser's local storagelocal_storage_remove_all() - Remove all key-value pairs from browser's local storageThe compact profile is the default and exposes the same browser capabilities through 11 tools and a smaller tools/list payload. It remains under evaluation; use --tool-profile legacy when an existing client still depends on the original 24 names. Legacy removal, if ever planned, will be announced as a separate breaking lifecycle change. Legacy names are not advertised as compact aliases because aliases would keep their schemas in Agent context.
Start it with:
python -m mcp_server_selenium
# Explicit compatibility fallback
python -m mcp_server_selenium --tool-profile legacyAll compact calls share one browser session and global active tab. A reliable default workflow is:
tabs(list) -> navigate -> wait_for -> query_elements -> interact_element -> take_screenshotUse only fields relevant to an action. The 24 legacy names map to compact as follows:
| Legacy tool | Compact call |
|---|---|
navigate(url, timeout) | navigate(url, wait_until="complete", timeout=timeout) |
list_tabs() | tabs(action="list") |
open_tab(url) | tabs(action="open", url=url) |
switch_tab(handle) | tabs(action="switch", handle=handle) |
close_tab(handle) | tabs(action="close", handle=handle) |
take_screenshot(file_name, directory) | take_screenshot(file_name, directory, mode="viewport") |
record_video(action, file_name, directory, include_address, window_handle, max_duration_seconds, window_handles) | Same record_video(...) contract in both profiles |
check_page_ready(wait_seconds) | wait_for(condition="ready", state="complete") |
get_console_logs(log_level) | browser_logs(action="console", log_level=log_level) |
get_network_logs(filter_url_by_text, only_errors_log) | browser_logs(action="network", filter_url_by_text=..., only_errors_log=...) |
get_response(request_id) | browser_logs(action="response", request_id=request_id) |
local_storage_add(...) | local_storage(action="add", key=..., string_value=... or object_value=...) |
local_storage_read(key) | local_storage(action="read", key=key) |
local_storage_remove(key) | local_storage(action="remove", key=key) |
local_storage_read_all() | local_storage(action="read_all") |
local_storage_remove_all() | local_storage(action="remove_all") |
get_an_element(...) | query_elements(action="one", selector=...) |
get_elements(...) | query_elements(action="many", selector=...) |
get_direct_children(...) | query_elements(action="children", selector=...) |
click_to_element(...) | Query, then interact_element(action="click", element_ref=...) |
set_value_to_input_element(...) | Query, then interact_element(action="set_value", element_ref=..., input_value=...) |
run_javascript_in_console(javascript_code) | run_javascript(javascript_code) |
run_javascript_and_get_console_output(javascript_code) | run_javascript(javascript_code, capture_console=true) |
get_style_an_element(...) | Query, then get_element_style(element_ref=...) |
Each line is one JSON arguments object for the workflow step in the same order:
{"action":"list"}
{"url":"https://example.com","wait_until":"network_idle","timeout":30,"quiet_ms":500}
{"condition":"element","state":"visible","selector":{"type":"css","value":"#login"},"timeout":10}
{"action":"one","selector":{"type":"css","value":"#login"}}
{"action":"click","element_ref":"el_VALUE_FROM_QUERY"}
{"file_name":"login-result","mode":"full_page"}tabs(action="list") also reports the active handle, URL/title/readyState of every tab, Chrome and ChromeDriver versions, and the absolute download directory. open activates its new tab; switch requires a listed handle; close accepts a handle or the active tab but never the final tab. Serialize tab-sensitive calls.
query_elements and element/text wait_for accept one discriminated selector:
{"type":"xpath","value":"//button[@type='submit']"}
{"type":"css","value":"form.login button.primary","frame":"payment-frame"}
{"type":"fields","value":{"element_type":"input","id":"email","attribute:data-test":"login-email"}}
{"type":"fields","value":{"element_type":"article","role":"article","accessible_name":"Assistant message"}}
{"type":"ref","value":"el_VALUE_FROM_QUERY"}Fields combine with AND; role and accessible_name use Selenium's computed accessibility values. frame tries iframe ID, then name. For ordered nested traversal, use up to eight options.scope steps; frame steps support CSS/ID/name and shadow steps enter open shadow roots:
{"action":"one","selector":{"type":"css","value":"button.allow"},"options":{"scope":[{"type":"frame","value":"payment-frame","by":"id"},{"type":"shadow","value":"#permission-host","by":"css"}]}}XPath is supported in documents/frames; use CSS or fields inside shadow roots. Query pages are 1-based; the default sizes are 3 for many and 5 for children, with a maximum of 50. one requires exactly one match and children exactly one parent. Every result element has an opaque element_ref. Queries include hidden DOM matches and report visible separately. Refs store locator specifications, not WebElements, and re-resolve after same-document rerenders while the exact locator still matches one element. They expire after 1,800 seconds and become invalid after a tab/document change, full navigation, or a zero/multiple current match; query again in those cases.
wait_for.condition supports:
| Condition | Required/action fields | |||
|---|---|---|---|---|
ready | `state="interactive | complete"` | ||
url | value, optional `match="contains | equals | regex"` | |
element | selector, `state="present | visible | enabled | hidden"` |
text | value, optional selector, `state="present | absent" and match` | ||
network_idle | optional quiet_ms; options.ignore_url_regexes excludes known polling routes | |||
network_response | options.cursor, route filters, and optional dotted-path json_predicate | |||
all, any | options.conditions, 1-10 bounded nested clauses with one shared deadline |
For a selector-free text wait, the observed value is the complete document.body.innerText. With a selector, it is every matched element's .text joined with newline characters. Therefore match="equals" compares that entire exact string, including browser-produced whitespace and newlines; use contains for a fragment.
Network-response waits peek without consuming diagnostic events. Take a baseline latest_cursor from browser_logs, trigger the request, then wait after that cursor:
{"condition":"network_response","timeout":30,"options":{"cursor":120,"filters":{"url_regex":"/api/build(?:\\?|$)","method":"GET","resource_type":"Fetch","status":200},"json_predicate":{"partial":false}}}network_idle ignores long-lived EventSource/WebSocket requests but normal polling creates fresh finite requests; declare only known polling routes in ignore_url_regexes or prefer network_response. timeout is the deadline for the complete tool call/composed wait, not for each poll or clause. timeout and poll_interval are seconds; quiet_ms is milliseconds. Success and timeout responses include elapsed time, URL, ready state, and the last observation. navigate.wait_until accepts initiated, interactive, complete, or network_idle; completed policies return the final URL after redirects, while initiated explicitly reports that navigation remains pending.
All actions except viewport/nested scroll require element_ref. Action-specific fields are:
| Action | Additional fields | |||
|---|---|---|---|---|
inspect | none; returns CSS/geometry/hit-test/blocker/inert/animation/scroll-ancestor diagnostics | |||
click | optional `options.click_mode="dom | native | actions | javascript", offset, stability_ms, observe_ms` |
clear, hover, scroll_into_view | none | |||
type, set_value | input_value; options for target/input mode, clear, synthetic paste input type, verification | |||
press_key | key, for example ENTER, TAB, or ARROW_DOWN | |||
select_option | `option_by="value | text | index", option_value` | |
upload_file | existing absolute or workspace-relative file_path | |||
scroll | optional container ref; direction/amount/start/end, until_visible, nested sweep and step budget in options |
Click defaults to dom: it re-resolves the ref, centers it, waits for a stable actionable hit-test, and retries stale/temporarily non-interactable states within the one overall timeout, then dispatches pointerdown/mousedown, focus, pointerup/mouseup, and click() in the page. These events are untrusted (isTrusted=false), but they never make Chrome raise its window or take desktop focus. native sends a trusted WebDriver click (Chrome activates its window; see Desktop focus), actions adds pointer offsets, and javascript is a bare element.click() that skips actionability checks. Failure reports the covering element and reasons such as hidden, offscreen, inert, animation, or pointer-events:none. No mode is ever a silent fallback.
type appends by default and set_value replaces. target_kind="auto|form_control|contenteditable" and input_mode="auto|keyboard|dom" support inputs, textareas, and ProseMirror-style contenteditable roots. DOM rich-text insertion dispatches cancelable beforeinput, then input/change; input_type="insertFromPaste" supplies synthetic paste semantics without reading or changing the system clipboard. Verification is on by default and compares .value or rendered editor text; a mismatch returns input_postcondition_failed.
ok=true means the requested browser command completed and any declared postcondition passed. It does not assert application intent. execution reports the actual strategy and reported events; observed separately reports URL, target state, focus/selection, bounded DOM mutations, effect_observed, and always application_outcome="not_asserted".
scroll is server-controlled rather than a long-running page loop. It can operate on one container ref or scan bounded nested containers, stop when a selector/ref is truly visible by viewport hit-test, and use direction="both" for a down-then-up sweep.
get_element_style(element_ref, ...) requires one ref. return_html=true returns only bounded inner/outer HTML and overrides the style flags. Otherwise all_styles and computed_style independently add their bounded sections; both false returns element metadata only.
Screenshots use mode="viewport|full_page|element"; element mode requires a ref. Names are safe PNG basenames and collisions get numeric suffixes. Directories may be absolute or workspace-relative without traversal. Chrome downloads default to <workspace>/tmp/selenium-downloads; configure them with --download_dir.
Video recording is stateful: call record_video(action="start", file_name=..., directory=..., include_address=..., window_handle=..., max_duration_seconds=..., window_handles=...), continue interacting with the browser, then always call stop once immediately after the final browser action. Pass window_handle for one exact fixed tab, or window_handles for 2-4 unique handles returned by tabs(action="list")/list_tabs(); the active handle at start must be included in that list. The multi-tab mode publishes one MP4 and changes its visible source whenever tabs(action="switch") or switch_tab() activates another selected handle. These arguments are mutually exclusive; omit both to record the current tab. Stop before closing any selected tab or shutting down the session. Only one recording may run per server process. Names are safe MP4 basenames, the default directory is <workspace>/tmp/selenium-video, collisions get numeric suffixes, and incomplete files are never published.
max_duration_seconds defaults to 600 and must be 0.1-86,400. It is a forgotten-cleanup fallback, not a replacement for stop: the server auto-finalizes at the deadline. start returns cleanup_required and deadline_at; status adds elapsed/remaining time; the final result records stop_reason as manual, deadline, or server shutdown.
For a multi-tab start, all selected targets provide frames to one live encoder, but only the logically active selected tab is written to the timeline. For example, a flow that switches A → B → A produces one checkout.mp4 showing those same scenes in order. Use the server's tab tools for switching so the recorder receives the transition; activating a tab outside window_handles freezes the video on the last selected tab until the flow returns. The multi-tab mode requires ffmpeg; combined mosaic output is not supported.
include_address=false records tab content only. include_address=true uses ffmpeg to add a synthetic header containing the visible top-level URL; in follow-active mode the header updates with selected-tab switches and top-level navigations. It does not capture Chrome's real toolbar or omnibox. The native Chrome recording command is experimental and recent; older builds automatically use a tab-bound screencast/ffmpeg fallback. unsupported_browser is returned only when neither transport is available. The URL-header mode and the compatibility fallback require ffmpeg on PATH.
Combined multi-tab and address-header flow in the compact profile:
{"tool":"tabs","arguments":{"action":"list"}}
{"tool":"record_video","arguments":{"action":"start","file_name":"checkout-flow","directory":"/absolute/workspace/evidence","include_address":true,"window_handles":["HANDLE_A","HANDLE_B"],"max_duration_seconds":900}}
{"tool":"tabs","arguments":{"action":"switch","handle":"HANDLE_B"}}
{"tool":"tabs","arguments":{"action":"switch","handle":"HANDLE_A"}}
{"tool":"record_video","arguments":{"action":"stop"}}This produces one checkout-flow.mp4 whose timeline and synthetic URL header both follow A → B → A. Perform the browser interactions for each tab between its switch and the next switch, and call stop immediately after the final interaction.
Console/network log calls accept mode="peek|consume", cursor, limit (1-100), and since_timestamp (epoch milliseconds). peek preserves returned entries; consume removes them. Continue with next_cursor. Buffers and cursors are browser-session local and bounded. Console levels are blank/ALL, DEBUG, INFO, WARNING, ERROR, or SEVERE (ERROR aliases Chrome SEVERE). Network filters support URL regex, HTTP method, resource type, request ID, and status in addition to legacy URL-text/error filters. Correlated metadata lets response/finished events filter on original request properties. Redaction is on by default for credentials, sensitive query values, and request body/header text; redact=false is an explicit sensitive-data opt-in.
{"action":"console","mode":"peek","cursor":0,"limit":20,"log_level":"ERROR"}
{"action":"network","mode":"consume","cursor":0,"limit":50,"filter_url_by_text":"/api/","redact":true}
{"action":"network","mode":"peek","event_type":"response","filters":{"url_regex":"/api/build","method":"GET","resource_type":"Fetch","status":200},"redact":true}
{"action":"response","request_id":"CDP_REQUEST_ID"}Response bodies can expire or be evicted from Chrome's CDP buffer; error envelopes give a stable reason code and retry hint. Bodies themselves may contain sensitive data.
run_javascript awaits Promises and returns bounded typed values for undefined, null, primitives, DOM elements, arrays/objects, circular/max-depth values, and exceptions. capture_console=false never reads logs. true returns only newly generated entries and keeps older entries in the broker. Example:
{"javascript_code":"return await Promise.resolve(document.querySelector('h1'))","capture_console":false,"timeout":30}Storage add requires key plus a value or explicit empty-value flag; object mode wins over string mode. read/remove require key; read_all/remove_all need no extra field. localStorage is scoped to the active page origin.
Treat screenshots, videos, visible URLs, uploads, downloads, response bodies, raw logs, tabs, and localStorage as potentially sensitive. They can contain credentials, PHI, or data from another origin in the shared session. Use explicit artifact paths and redact=false only when authorized.
Each line below is one complete JSON arguments object for the named tool. Replace handle, reference, request-ID, path, URL, and expected-text placeholders with observed values.
#### tabs
{"action":"list"}
{"action":"open","url":"https://example.com"}
{"action":"switch","handle":"TAB_HANDLE_FROM_LIST"}
{"action":"close","handle":"TAB_HANDLE_FROM_LIST"}#### navigate
{"url":"https://example.com","wait_until":"initiated","timeout":30}
{"url":"https://example.com","wait_until":"interactive","timeout":30}
{"url":"https://example.com","wait_until":"complete","timeout":30}
{"url":"https://example.com","wait_until":"network_idle","timeout":30,"quiet_ms":500}#### wait_for
{"condition":"ready","state":"complete","timeout":10}
{"condition":"url","value":"/dashboard","match":"contains","timeout":10}
{"condition":"element","state":"visible","selector":{"type":"css","value":"#login"},"timeout":10}
{"condition":"text","state":"present","value":"Welcome","selector":{"type":"css","value":"main"},"match":"contains","timeout":10}
{"condition":"network_idle","quiet_ms":500,"timeout":10}
{"condition":"network_idle","quiet_ms":500,"timeout":10,"options":{"ignore_url_regexes":["/api/poll(?:\\?|$)"]}}
{"condition":"network_response","timeout":30,"options":{"cursor":120,"filters":{"url_regex":"/api/build","method":"GET","status":200},"json_predicate":{"partial":false}}}
{"condition":"all","timeout":30,"options":{"conditions":[{"condition":"element","state":"visible","selector":{"type":"css","value":"main"}},{"condition":"network_response","options":{"cursor":120,"filters":{"url_regex":"/api/build"}}}]}}
{"condition":"any","timeout":30,"options":{"conditions":[{"condition":"text","state":"present","value":"Done"},{"condition":"url","value":"/complete"}]}}#### query_elements
{"action":"one","selector":{"type":"css","value":"#login"}}
{"action":"many","selector":{"type":"fields","value":{"element_type":"button"}},"page":1,"page_size":10}
{"action":"children","selector":{"type":"ref","value":"el_PARENT_FROM_QUERY"},"page":1,"page_size":10}
{"action":"one","selector":{"type":"fields","value":{"element_type":"article","role":"article","accessible_name":"Assistant message"}}}
{"action":"one","selector":{"type":"css","value":"button.allow"},"options":{"scope":[{"type":"frame","value":"app-frame","by":"id"},{"type":"shadow","value":"#gate-host"}]}}#### interact_element
{"action":"inspect","element_ref":"el_FROM_QUERY"}
{"action":"click","element_ref":"el_FROM_QUERY"}
{"action":"click","element_ref":"el_FROM_QUERY","options":{"click_mode":"native"}}
{"action":"click","element_ref":"el_FROM_QUERY","options":{"click_mode":"actions","offset":{"x":0,"y":0},"stability_ms":100}}
{"action":"click","element_ref":"el_FROM_QUERY","options":{"click_mode":"javascript"}}
{"action":"clear","element_ref":"el_FROM_QUERY"}
{"action":"type","element_ref":"el_FROM_QUERY","input_value":"hello"}
{"action":"set_value","element_ref":"el_FROM_QUERY","input_value":"hello"}
{"action":"set_value","element_ref":"el_FROM_QUERY","input_value":"hello","options":{"target_kind":"contenteditable","input_mode":"auto","clear_existing":true,"verify_after_input":true}}
{"action":"press_key","element_ref":"el_FROM_QUERY","key":"ENTER"}
{"action":"select_option","element_ref":"el_FROM_QUERY","option_by":"value","option_value":"active"}
{"action":"hover","element_ref":"el_FROM_QUERY"}
{"action":"scroll_into_view","element_ref":"el_FROM_QUERY"}
{"action":"scroll","element_ref":"el_SCROLL_CONTAINER","timeout":10,"options":{"direction":"down","amount":400,"until_visible":{"type":"ref","value":"el_TARGET"},"max_steps":20}}
{"action":"scroll","timeout":10,"options":{"direction":"both","amount":500,"until_visible":{"type":"css","value":"button.allow"},"include_nested_scroll_containers":true,"max_steps":30}}
{"action":"upload_file","element_ref":"el_FROM_QUERY","file_path":"/absolute/path/to/file.txt"}#### take_screenshot
{"file_name":"page","mode":"viewport"}
{"file_name":"full-page","mode":"full_page"}
{"file_name":"component","mode":"element","element_ref":"el_FROM_QUERY"}#### record_video
{"action":"start","file_name":"checkout-flow","include_address":false,"max_duration_seconds":600}
{"action":"start","file_name":"checkout-with-url","directory":"/absolute/workspace/evidence","include_address":true,"window_handle":"HANDLE_FROM_TABS_LIST","max_duration_seconds":1800}
{"action":"start","file_name":"checkout-tabs-with-url","directory":"/absolute/workspace/evidence","include_address":true,"window_handles":["FIRST_HANDLE","SECOND_HANDLE"],"max_duration_seconds":1800}
{"action":"status"}
{"action":"stop"}#### browser_logs
{"action":"console","mode":"peek","cursor":0,"limit":20,"log_level":"ERROR"}
{"action":"network","mode":"peek","cursor":0,"limit":20,"event_type":"response","filter_url_by_text":"/api/","redact":true}
{"action":"network","mode":"peek","event_type":"response","filters":{"url_regex":"/api/build","method":"GET","resource_type":"Fetch","status":200},"redact":true}
{"action":"response","request_id":"CDP_REQUEST_ID"}#### local_storage
{"action":"add","key":"settings","object_value":{"theme":"dark"}}
{"action":"read","key":"settings"}
{"action":"remove","key":"settings"}
{"action":"read_all"}
{"action":"remove_all"}#### get_element_style
{"element_ref":"el_FROM_QUERY","all_styles":false,"computed_style":false}
{"element_ref":"el_FROM_QUERY","all_styles":true,"computed_style":true}
{"element_ref":"el_FROM_QUERY","return_html":true}#### run_javascript
{"javascript_code":"return document.title","capture_console":false,"timeout":30}
{"javascript_code":"console.warn('probe'); return location.href","capture_console":true,"timeout":30}ffmpeg on PATH for include_address=true and for Chrome builds that need the screencast compatibility fallbackYou can use this MCP server in two ways:
Install directly from PyPI:
pip install mcp-server-seleniumOr using uv:
uv add mcp-server-seleniumgit clone https://github.com/PhungXuanAnh/selenium-mcp-server.git
cd selenium-mcp-serveruv syncOr using pip with virtual environment:
python -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
pip install -e .The MCP server can work with Chrome in two ways:
google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-debugDesktop focus. Tab listing, viewport screenshots, and closing background tabs use CDP calls that never raise the Chrome window. The default dom click never raises it either. Chrome itself still activates its window on launch, explicit click_mode="native" or "actions" mouse presses, and tab activation (open/switch/close the active tab). On Linux X11 with xdotool installed, the server returns focus to the window you were using within a fraction of a second, so Chrome may flash briefly but does not keep keyboard focus. The guard stays active for about 2 seconds after such a command, so clicking into Chrome yourself in that window is also reverted. Set SELENIUM_MCP_ALLOW_FOCUS_STEAL=1 to disable this restore.
After installing via pip/uv, you can run the server directly:
# Basic usage with the default 11-tool compact surface
python -m mcp_server_selenium
# Use the original 24 tool names for compatibility
python -m mcp_server_selenium --tool-profile legacy
# With custom Chrome debugging port and user data directory
python -m mcp_server_selenium --port 9222 --user_data_dir /tmp/chrome-debug
# With verbose logging
python -m mcp_server_selenium --port 9222 --user_data_dir /tmp/chrome-debug -v
# Using the installed command (if available)
selenium-mcp-server --port 9222 --user_data_dir /tmp/chrome-debug -vWhen running from source, ensure the Python path includes the src directory:
# Navigate to the project directory
cd /path/to/selenium-mcp-server
# Activate virtual environment (if using one)
source .venv/bin/activate
# Run with proper Python path
PYTHONPATH=src python -m mcp_server_selenium --port 9222 --user_data_dir /tmp/chrome-debug -v
# Or using uv (recommended for development)
uv run python -m mcp_server_selenium --port 9222 --user_data_dir /tmp/chrome-debug -vFor development and testing, you can use the MCP inspector:
From Source Code:
# Using uv (recommended)
uv run mcp dev src/mcp_server_selenium/__main__.py
# Or with make command
make inspector
# With custom options
uv run mcp dev src/mcp_server_selenium/__main__.py --port 9222 --user_data_dir /tmp/chrome-debug --verboseFrom Installed Package:
# Create a wrapper script or use directly
mcp dev python -m mcp_server_seleniumOpen your browser and navigate to: http://127.0.0.1:6274/#tools
Check logs:
tailf /tmp/selenium-mcp.log--port: Chrome remote debugging port (default: 9222)--user_data_dir: Chrome user data directory (default: auto-generated in /tmp)--workspace_root: Base directory for relative screenshot/video directories (default: the server startup directory). Absolute artifact directories do not use it.--tool-profile: Agent-visible tool surface: compact (default) or legacy compatibility fallback--download_dir: Chrome download directory; defaults to <workspace_root>/tmp/selenium-downloads-v, --verbose: Increase verbosity (use multiple times for more details)The server communicates via stdio and follows the Model Context Protocol specification. You can integrate it with MCP-compatible AI assistants or clients.
For Claude Desktop (claude_desktop_config.json):
Using installed package:
{
"mcpServers": {
"selenium": {
"command": "python",
"args": [
"-m", "mcp_server_selenium",
"--port", "9222",
"--user_data_dir", "/tmp/chrome-debug-claude"
],
"env": {}
}
}
}For VS Code Copilot (.vscode/mcp.json):
Using installed package:
{
"servers": {
"selenium-installed": {
"command": "python",
"args": [
"-m", "mcp_server_selenium",
"--user_data_dir=/home/user/.selenium-mcp-server-chrome-profile/default",
"--port=9225"
]
}
}
}Using source code directly:
{
"servers": {
"selenium-source": {
"command": "/path/to/selenium-mcp-server/.venv/bin/python",
"args": [
"-m", "mcp_server_selenium",
"--user_data_dir=/home/user/.selenium-mcp-server-chrome-profile/source",
"--workspace_root=/path/to/workspace",
"--port=9226"
],
"env": {
"PYTHONPATH": "/path/to/selenium-mcp-server/src"
}
}
}
}Alternative source code configuration using full path:
{
"servers": {
"selenium-source-alt": {
"command": "/path/to/selenium-mcp-server/.venv/bin/python",
"args": [
"/path/to/selenium-mcp-server/src/mcp_server_selenium/__main__.py",
"--user_data_dir=/home/user/.selenium-mcp-server-chrome-profile/alt",
"--port=9227"
]
}
}
}Omit --tool-profile to use the default compact surface. Add "--tool-profile", "legacy" to a client's server args only when it requires the original 24 tool names.
VS Code Copilot MCP Status: If you open the .vscode/mcp.json file, you can see the MCP server status at the bottom of VS Code.
alt text
View MCP Logs:
tail -f /tmp/selenium-mcp.lognavigatehttps://example.comtake_screenshotexample-home<absolute-workspace-path>/tmp/selenium-screenshot; omit it when the workspace path is unavailable<absolute-workspace-path>/tmp/selenium-screenshot/example-home.pngset_value_to_input_elementxpath="//*[@id='email']", input_value="[email protected]"click_to_elementxpath="//button[@type='submit']"run_javascript_in_consolereturn document.title;run_javascript_and_get_console_outputconsole.log('Hello from browser'); return window.location.href;check_page_ready; query the target again when dynamic content is expectedrun_javascript_in_console with explicit return expressionsget_an_element, get_elements, or get_style_an_elementrun_javascript_in_console for complex DOM manipulation and data extractionrun_javascript_and_get_console_output to capture console logs for debuggingExtract page data:
// Tool: run_javascript_in_console
var links = Array.from(document.querySelectorAll('a')).slice(0, 5).map(a => ({
text: a.textContent.trim(),
href: a.href
}));
return links;Page performance monitoring:
// Tool: run_javascript_and_get_console_output
console.time('Page Analysis');
var stats = {
title: document.title,
links: document.querySelectorAll('a').length,
images: document.querySelectorAll('img').length,
scripts: document.querySelectorAll('script').length
};
console.timeEnd('Page Analysis');
console.log('Page stats:', stats);
return stats;Form automation:
// Tool: run_javascript_in_console
document.querySelector('#username').value = 'testuser';
document.querySelector('#password').value = 'password123';
document.querySelector('#login-form').submit();
return 'Form submitted successfully';The server logs all operations to /tmp/selenium-mcp.log with rotation. Use the -v flag to increase console verbosity:
-v: INFO level logging-vv: DEBUG level loggingPackage not found (installed package):
# Verify installation
pip list | grep mcp-server-selenium
# or
python -c "import mcp_server_selenium; print('OK')"Module not found (source code):
# Ensure PYTHONPATH is set correctly
export PYTHONPATH=/path/to/selenium-mcp-server/src
# or run from project root with:
PYTHONPATH=src python -m mcp_server_selenium--port optionMCP Client Connection Problems:
which python to find Python executable)Run the focused tests without starting Chrome:
PYTHONPATH=src .venv/bin/python -m unittest discover -s tests -p "test_*.py" -vRun the MCP stdio end-to-end test, which starts the real server and Chrome with an isolated temporary profile:
.venv/bin/python tests/mcp_stdio_e2e.py -vFor issues and questions:
/tmp/selenium-mcp.logSee Available Tools, including the complete compact migration table, and Examples.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.