diff --git a/mcp_servers/README.md b/mcp_servers/README.md index 7426c7096ad..6d168043c73 100644 --- a/mcp_servers/README.md +++ b/mcp_servers/README.md @@ -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` @@ -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)` | @@ -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). diff --git a/mcp_servers/pyproject.toml b/mcp_servers/pyproject.toml index 87870e7176c..739bc1f57b9 100644 --- a/mcp_servers/pyproject.toml +++ b/mcp_servers/pyproject.toml @@ -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" diff --git a/mcp_servers/server.py b/mcp_servers/server.py index 2e3fedc6147..8f794588645 100644 --- a/mcp_servers/server.py +++ b/mcp_servers/server.py @@ -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 @@ -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() @@ -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}" @@ -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() diff --git a/requirements.txt b/requirements.txt index 50f534cbed5..0a6ea4827c0 100755 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py index 758eedb57cc..70e4fe4a4fb 100755 --- a/seleniumbase/__version__.py +++ b/seleniumbase/__version__.py @@ -1,2 +1,2 @@ # seleniumbase package -__version__ = "4.53.3" +__version__ = "4.53.4" diff --git a/setup.py b/setup.py index a4608e0f1aa..b3a50c95a6e 100755 --- a/setup.py +++ b/setup.py @@ -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',