Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions mcp_servers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,10 @@ Restart Claude Desktop. You should see a 🔨 tools icon indicating the server c
* `get_attributes`
* `check_state`
* `click`
* `hover`
* `drag_and_drop`
* `hover_with_action`
* `fill_input`
* `select_option`
* `act_on_element`
* `focus_on`
* `wait_for`
* `assert_that`
* `manage_cookies`
Expand Down Expand Up @@ -150,14 +149,14 @@ claude mcp add seleniumbase-mcp -- uv run seleniumbase-mcp

## Tools exposed

Tools here are grouped around a shared `selector` convention: `selector` args accept a CSS selector, or visible text (e.g. `a:contains("Sign in")`). Several near-identical one-off tools (e.g. separate click/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.
Tools here are grouped around a shared `selector` convention: `selector` args accept a CSS selector, or visible text (e.g. `a:contains("Sign in")`). 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`, `navigate_history(action: back/forward/reload)`, `get_page_info` (running status, url, title, origin, user agent, history in one call) |
| Finding & reading | `find_elements(selector, timeout, include_html)`, `get_content(selector, output_format: text/html/urls, include_shadow_dom)`, `get_attributes`, `check_state(check: present/visible/count/text_visible)` |
| Interacting | `click(selector, nth, all_matches, only_if_visible, parent_selector, timeout, scroll)`, `hover(selector, then_click_selector)`, `drag_and_drop`, `fill_input(mode: type/append/set_value/fast_type/clear)`, `select_option(by: text/value/index)`, `act_on_element(action: focus/highlight/scroll_into_view)` |
| Interacting | `click(selector, nth, all_matches, only_if_visible, parent_selector, timeout, scroll)`, `hover_with_action(selector1, selector2, action: none/click/drag_and_drop)`, `fill_input(mode: type/append/set_value/fast_type/clear)`, `select_option(by: text/value/index)`, `focus_on(action: scroll_to_element/focus/highlight)` |
| Waiting | `wait_for(state: present/visible/not_visible/absent, text)` |
| Assertions | `assert_that(check: element_present/element_visible/text/title/url/url_contains)` |
| Cookies & storage | `manage_cookies(action: get_all/clear/save/load)`, `manage_storage(storage: local/session, action: get/set)` |
Expand All @@ -174,6 +173,14 @@ Tools here are grouped around a shared `selector` convention: `selector` args ac

- **Errors surface as descriptive strings.** Every tool (aside from session-lifecycle tools, which handle their own errors) is wrapped by a `handle_sb_errors` decorator: if a selector isn't found or an assertion fails, `sb_cdp.Chrome` raises an exception, and the decorator catches it and returns a string like `Error in click: NoSuchElementException - ...` instead of a raw tool error. This lets the calling agent read the failure and self-correct (e.g. by waiting longer or trying a different selector) rather than just seeing an opaque tool-call failure.

- **No standalone session-status tool.** There is no separate `browser_status`-style tool. `get_page_info` doubles as the status check: it returns `{"running": False}` (optionally with an `error` field) when there's no active session or the session errors out, and full page metadata (`running: True`, `url`, `title`, `origin`, `user_agent`, `history`) otherwise.

- **Content reading is consolidated into one tool.** `get_content` replaces what used to be three separate reads: page/element text, page/element HTML, and page-linked URLs. Pick the mode with `output_format` (`"text"`, `"html"`, or `"urls"`) rather than calling a dedicated `get_page_content` or `get_all_urls` tool — those no longer exist. Likewise, there's no standalone `get_user_agent` tool anymore; the User-Agent string is one of the fields returned by `get_page_info`.

- **Hover, click-after-hover, and drag-and-drop share one tool.** `hover_with_action(selector1, selector2, action)` replaces the earlier separate `hover` and `drag_and_drop` tools. `action="none"` hovers `selector1` only; `action="click"` hovers `selector1` then clicks `selector2` (useful for dropdown/submenu items revealed by hovering); `action="drag_and_drop"` drags `selector1` onto `selector2`. (`selector2` is required when `action` is `"click"` or `"drag_and_drop"`.)

- **Non-activating element actions are `focus_on`.** What used to be `act_on_element` is now `focus_on(selector, action)`, with actions `scroll_to_element` (the default), `focus`, and `highlight` — note the default action changed from focusing the element to scrolling it into view. None of these actions click, type into, select from, or otherwise activate the element; use `click`, `fill_input`, `select_option`, or `hover_with_action` for that.

- **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, so `find_elements` resolves each match immediately to a plain dict (`tag_name`, `text`, and optionally `html`) instead of returning a handle you could call further methods on. If you need to act on one of several matches, use `click(selector, nth=...)` (acts by position) rather than "find, then click" as two separate steps.

- **CAPTCHA-solving.** `solve_captcha` handles supported challenge types (e.g. Cloudflare Turnstile).
Expand Down
2 changes: 1 addition & 1 deletion mcp_servers/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "seleniumbase-mcp"
version = "1.2.1dev1"
version = "1.2.2dev0"
description = "MCP server exposing SeleniumBase CDP Mode as tools for MCP clients."
readme = "README.md"
requires-python = ">=3.10"
Expand Down
146 changes: 86 additions & 60 deletions mcp_servers/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,8 @@
- Use wait_for when the agent needs to wait for a condition to become true.
- Use assert_that when the agent needs to verify an expected condition and
treat failure as an assertion error.
- Use click/fill_input/select_option/hover/act_on_element for interactions.

Note on elements: CDP-mode element objects (from find_element/find_all) are
live handles with their own methods (.click(), .get_html(), ...) that can't
cross the MCP boundary as stateful objects. Tools here resolve elements
immediately to plain dicts (tag, text, html) rather than returning handles.
To act on one of several matches, use click(selector, nth=...) rather than
find + click.
- Use click/fill_input/select_option/hover_with_action/focus_on for
interactions and element positioning.
"""
from __future__ import annotations
import atexit
Expand Down Expand Up @@ -708,54 +702,84 @@ def click(

@mcp.tool()
@handle_sb_errors
def hover(
selector: str,
then_click_selector: str | None = None,
def hover_with_action(
selector1: str,
selector2: str | None = None,
action: Literal[
"none",
"click",
"drag_and_drop",
] = "none",
) -> str:
"""Hover over an element, optionally clicking an element revealed by hover.
"""Hover over an element, optionally click another element, or drag-&-drop.

Use this for menus, dropdowns, tooltips, or other interfaces where an
element must first be hovered before its target becomes available.
Use this tool for hover interactions, hover-triggered menus, and
drag-and-drop operations.

Args:
selector: Element to hover over.
then_click_selector: Optional element to click after the hover.
Useful for a submenu item or dropdown option revealed by hover.
selector1:
The primary element selector.

Returns:
A confirmation describing the hover/click operation.
"""
sb = _get_sb()
For action="none", this is the element to hover over.

if then_click_selector:
sb.hover_and_click(selector, then_click_selector)
return f"Hovered {selector} and clicked {then_click_selector}"
For action="click", this is the element to hover over before
clicking selector2.

sb.hover_element(selector)
return f"Hovered {selector}"
For action="drag_and_drop", this is the draggable source element.

selector2:
The secondary element selector.

@mcp.tool()
@handle_sb_errors
def drag_and_drop(
source_selector: str,
target_selector: str,
) -> str:
"""Drag a draggable element and drop it onto another element.
Required for action="click", where it identifies the element
revealed or targeted after hovering selector1.

Drag-and-drop is performed through SeleniumBase's CDP browser controls,
simulating the pointer and mouse events expected by web applications.
Required for action="drag_and_drop", where it identifies the
destination/drop target.

Args:
source_selector: CSS selector identifying the draggable source.
target_selector: CSS selector identifying the drop target.
Not used for action="none".

action:
- "none": Hover over selector1 only.
- "click": Hover over selector1, then click selector2.
- "drag_and_drop": Drag selector1 and drop it onto selector2.

Returns:
A confirmation describing the performed operation.

Tool selection:
- Simple hover -> action="none".
- Hover over one element and then click another -> action="click".
- Drag one element onto another -> action="drag_and_drop".

Notes:
For action="click", selector1 is the hover target and selector2 is
the click target.

The simulated interaction includes events such as pointerdown,
mousedown, dragstart, dragenter, dragover, drop, dragend, mouseup, and
pointerup.
For action="drag_and_drop", selector1 is the source and selector2
is the destination.
"""
_get_sb().drag_and_drop(source_selector, target_selector)
return f"Dragged {source_selector} onto {target_selector}"
sb = _get_sb()

if action == "none":
sb.hover_element(selector1)
return f"Hovered {selector1}"

if action == "click":
if selector2 is None:
return "Error: action='click' requires selector2."
sb.hover_and_click(selector1, selector2)
return f"Hovered {selector1} and clicked {selector2}"

if action == "drag_and_drop":
if selector2 is None:
return "Error: action='drag_and_drop' requires selector2."
sb.drag_and_drop(selector1, selector2)
return f"Dragged {selector1} onto {selector2}"

return (
f"Error: unknown action '{action}'. "
"Use 'none', 'click', or 'drag_and_drop'."
)


@mcp.tool()
Expand Down Expand Up @@ -860,50 +884,52 @@ def select_option(

@mcp.tool()
@handle_sb_errors
def act_on_element(
def focus_on(
selector: str,
action: Literal[
"scroll_to_element",
"focus",
"highlight",
"scroll_into_view",
] = "focus",
] = "scroll_to_element",
) -> str:
"""Perform a non-click positioning or debugging action on an element.
"""Scroll to, focus, or highlight an element.

Use this tool when an element needs to be focused, highlighted for human
observation/debugging, or scrolled into the viewport.
Use this tool when an element needs to be brought into view, focused for
keyboard interaction, or highlighted for debugging/demonstration.

This tool does NOT click, type into, select from, hover over, or otherwise
activate the element.

Args:
selector: CSS selector or SeleniumBase selector identifying the target.

action:
- "scroll_to_element": Scroll the page until the element is in
the current viewport. This is the default action.
- "focus": Move keyboard focus to the element.
- "highlight": Temporarily highlight the element for debugging or
demonstration. This can affect timing and may reduce stealth.
- "scroll_into_view": Scroll the page until the element is in the
current viewport.

Tool selection:
- Bring an element into view -> use focus_on with the default action.
- Focus an element -> use focus_on(action="focus").
- Highlight element for debugging -> use focus_on(action="highlight").
- Click -> use click.
- Type into a form control -> use fill_input.
- Hover -> use hover.
- Focus, highlight, or scroll without activating -> use
act_on_element.
- Hover -> use hover_with_action.
"""
sb = _get_sb()

if action == "focus":
if action == "scroll_to_element":
sb.scroll_into_view(selector)
elif action == "focus":
sb.find_element(selector).focus()
elif action == "highlight":
sb.highlight(selector)
elif action == "scroll_into_view":
sb.scroll_into_view(selector)
else:
return (
f"Error: unknown action '{action}'. "
"Use 'focus', 'highlight', or 'scroll_into_view'."
"Use 'scroll_to_element', 'focus', or 'highlight'."
)

return f"{action} done for {selector}"
Expand Down Expand Up @@ -1221,8 +1247,8 @@ def scroll(
up/down scrolling. For example, amount=25 scrolls approximately
one quarter of the viewport height.

Use act_on_element(action="scroll_into_view") when the goal is to reveal
a specific element rather than scroll the page by a relative amount.
Use focus_on(action="scroll_to_element") when the goal is to reveal a
specific element rather than scroll the page by a relative amount.
"""
sb = _get_sb()

Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ filelock>=3.32.5
fasteners>=0.20
mycdp>=1.4.0
pynose>=1.5.5
platformdirs>=4.11.5
platformdirs>=4.11.7
typing-extensions>=4.16.0
sbvirtualdisplay>=1.4.0
MarkupSafe>=3.0.3
Expand Down
2 changes: 1 addition & 1 deletion seleniumbase/__version__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# seleniumbase package
__version__ = "4.53.3"
__version__ = "4.53.4"
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@
'fasteners>=0.20',
'mycdp>=1.4.0',
'pynose>=1.5.5',
'platformdirs>=4.11.5',
'platformdirs>=4.11.7',
'typing-extensions>=4.16.0',
'sbvirtualdisplay>=1.4.0',
'MarkupSafe>=3.0.3',
Expand Down