From fbc665f3cb19ea6e5eb4ed6ffcf4eec32e4d9580 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 15 Sep 2026 01:04:20 -0400 Subject: [PATCH 1/7] Add tool annotations to the MCP Server --- mcp_servers/server.py | 280 +++++++++++++++++++++++++++++++++++------- 1 file changed, 238 insertions(+), 42 deletions(-) diff --git a/mcp_servers/server.py b/mcp_servers/server.py index eadee581c33..ce5558adf79 100644 --- a/mcp_servers/server.py +++ b/mcp_servers/server.py @@ -51,6 +51,17 @@ - Use 'focus_element' for element positioning and visual focus. - Use 'solve_captcha' for clicking the checkbox of a CAPTCHA on the page. - Use 'save_page' for saving page output as a PNG, a PDF, or an HTML file. + +Tool annotations: +Where a tool has one consistent behavior, it declares MCP tool annotations +(title + read_only_hint/destructive_hint/idempotent_hint/open_world_hint) +so clients can make informed UX/confirmation decisions without calling it. +Several tools here consolidate multiple related actions behind a single +`action`/`mode` parameter (e.g. manage_cookies: get_all/clear/save/load). +Where those actions genuinely differ in kind -- some read-only, some +destructive, some writing to disk -- no single annotation value would be +honest for the whole tool, so annotations are intentionally omitted for +those (title is still provided). See the per-tool comments below. """ from __future__ import annotations import atexit @@ -59,6 +70,7 @@ from typing import Any, Literal from mcp.server import MCPServer from mcp.server.mcpserver.exceptions import ToolError +from mcp.types import ToolAnnotations from seleniumbase import sb_cdp mcp = MCPServer("seleniumbase-mcp") @@ -99,7 +111,17 @@ def wrapper(*args, **kwargs): # Session lifecycle # --------------------------------------------------------------------------- -@mcp.tool() +@mcp.tool( + title="Start Browser", + annotations=ToolAnnotations( + # Single, consistent behavior: launches (or no-ops if already + # running) a persistent browser session. + read_only_hint=False, + destructive_hint=False, + idempotent_hint=True, # No-ops with the same message if already up. + open_world_hint=True, # Launches a real browser onto the open web. + ), +) def start_browser( url: str | None = None, headless: Literal[False, True, None] = None, @@ -245,7 +267,17 @@ def start_browser( ) -@mcp.tool() +@mcp.tool( + title="Close Browser", + annotations=ToolAnnotations( + # Single, consistent behavior: Ends the persistent browser session + # (or no-ops if already stopped). + read_only_hint=False, + idempotent_hint=True, # Once closed, repeated calls have no further + # effect. + open_world_hint=False, # Local teardown of a server-owned resource. + ), +) def close_browser() -> str: """Close the active browser session and release browser resources. @@ -278,7 +310,15 @@ def close_browser() -> str: # Page information # --------------------------------------------------------------------------- -@mcp.tool() +@mcp.tool( + title="Get Page Info", + annotations=ToolAnnotations( + read_only_hint=True, + destructive_hint=False, + idempotent_hint=True, + open_world_hint=True, + ), +) @handle_sb_errors def get_page_info() -> dict[str, Any]: """Get current browser session and page metadata. @@ -287,7 +327,7 @@ def get_page_info() -> dict[str, Any]: is after navigation, clicks, form submissions, redirects, reloads, or tab switches. - This is a READ-ONLY metadata operation. It does not inspect arbitrary + This is a read-only metadata operation: It does not inspect arbitrary page content, find elements, check visibility, wait for conditions, or assert expected values. @@ -337,7 +377,15 @@ def get_page_info() -> dict[str, Any]: # Navigation # --------------------------------------------------------------------------- -@mcp.tool() +@mcp.tool( + title="Open URL", + annotations=ToolAnnotations( + read_only_hint=False, + destructive_hint=False, + idempotent_hint=False, + open_world_hint=True, + ), +) @handle_sb_errors def open_url(url: str) -> str: """Navigate the current browser tab to the URL provided. @@ -372,15 +420,18 @@ def open_url(url: str) -> str: return f"Navigated to {url}" -@mcp.tool() +@mcp.tool(title="Manage History") +# Annotations intentionally omitted: 'list' is a pure read, while +# 'back'/'forward'/'reload' each navigate and modify browser state -- no +# single read_only_hint value would be accurate for the whole tool. @handle_sb_errors def manage_history( action: Literal["back", "forward", "reload", "list"] = "list", ) -> str | dict[str, Any]: """Manage or inspect the current browser tab's navigation history. - Use 'back' or 'forward' for history navigation, 'reload' to refresh - while bypassing the cache, or 'list' to inspect history. + Use "back", "forward", or "reload" actions for history navigation. + Use "list" to inspect history. (This one is read-only.) Use 'open_url' for navigation to an arbitrary URL. Args: @@ -391,7 +442,7 @@ def manage_history( - "list": Return the current history position and entries. Navigation actions can trigger page loads or redirects. - Use get_page_info afterward to verify the resulting URL or title. + Use 'get_page_info' afterward to verify the resulting URL or title. """ sb = _get_sb() @@ -433,7 +484,15 @@ def manage_history( # Finding & reading # --------------------------------------------------------------------------- -@mcp.tool() +@mcp.tool( + title="Find Elements", + annotations=ToolAnnotations( + read_only_hint=True, + destructive_hint=False, + idempotent_hint=True, + open_world_hint=True, + ), +) @handle_sb_errors def find_elements( selector: str, @@ -514,7 +573,15 @@ def find_elements( } -@mcp.tool() +@mcp.tool( + title="Get Content", + annotations=ToolAnnotations( + read_only_hint=True, + destructive_hint=False, + idempotent_hint=True, + open_world_hint=True, + ), +) @handle_sb_errors def get_content( selector: str = "body", @@ -567,18 +634,24 @@ def get_content( ) -@mcp.tool() +@mcp.tool( + title="Get Attributes", + annotations=ToolAnnotations( + read_only_hint=True, + destructive_hint=False, + idempotent_hint=True, + open_world_hint=True, + ), +) @handle_sb_errors def get_attributes( selector: str, attribute: str | None = None, timeout: float = 5, ) -> str | dict[str, Any] | None: - """Read HTML attributes from the first matching element. - - Use this tool when you need the value of a specific HTML attribute, - or all HTML attributes of an element. Attributes could be something - such as href, src, value, class, id, name, type, aria-label, etc. + """Get a specific HTML attribute (or all attributes) from the + first-matching element. Examples of possible attributes include + href, src, value, class, id, name, type, aria-label, etc. Args: selector: CSS selector or SeleniumBase-supported XPath selector. @@ -597,7 +670,8 @@ def get_attributes( - Need to check element presence/visibility -> use 'check_if_condition'. - This is a read-only operation. + This is a read-only operation: It finds elements to get the requested data, + but it does not make any modifications to those elements. If there's no matching element found within the timeout, then @handle_sb_errors returns details from the exception raised. @@ -610,7 +684,15 @@ def get_attributes( return sb.get_element_attributes(selector, timeout=timeout) -@mcp.tool() +@mcp.tool( + title="Check Condition", + annotations=ToolAnnotations( + read_only_hint=True, + destructive_hint=False, + idempotent_hint=True, + open_world_hint=True, + ), +) @handle_sb_errors def check_if_condition( check: Literal["present", "visible"] = "visible", @@ -659,8 +741,6 @@ def check_if_condition( This tool does not intentionally wait for elements or text to appear. It is intended for checking the current state only. If page timing or asynchronous loading matters, use wait_for_condition instead. - - When `text` is provided, `check` is ignored. """ sb = _get_sb() @@ -682,7 +762,20 @@ def check_if_condition( # Interacting with elements # --------------------------------------------------------------------------- -@mcp.tool() +@mcp.tool( + title="Click Element", + annotations=ToolAnnotations( + # These settings are consistent across all click modes. The tool + # always performs a browser interaction, regardless of which matching + # element(s) are selected. + read_only_hint=False, # Clicking can modify browser/page state. + destructive_hint=False, # The generic click operation is not + # inherently destructive; the target may be. + idempotent_hint=False, # Repeating a click can produce a different + # result or trigger another action. + open_world_hint=True, # The tool operates on external webpages. + ), +) @handle_sb_errors def click_element( selector: str, @@ -699,12 +792,12 @@ def click_element( clicking all visible matches, conditional clicks, or clicks scoped to a parent element. - Selection behavior: - - `nth` is 1-based and takes precedence over every other click mode. - - Otherwise, `all_matches=True` clicks every currently visible match. - - Otherwise, `only_if_visible=True` clicks only if a match is visible. - - Otherwise, `parent_selector` scopes the click to a nested element. - - With none of the above, performs a normal SeleniumBase click. + Selection behavior and priority: + `nth` is 1-based and takes precedence over every other click mode. + Otherwise, `all_matches=True` clicks every currently visible match. + Otherwise, `only_if_visible=True` clicks only if a match is visible. + Otherwise, `parent_selector` scopes the click to a nested element. + If none of the above are set, then a regular click is performed. Args: selector: CSS selector, XPath selector, or supported SeleniumBase @@ -779,7 +872,11 @@ def click_element( return f"Clicked {selector}" -@mcp.tool() +@mcp.tool(title="Hover / Click / Drag") +# Annotations intentionally omitted: 'hover' alone is a near-harmless +# observation-adjacent action, while 'hover_and_click' and 'drag_and_drop' +# can modify or reorder page/data state (like click_element) -- no single +# destructive_hint value would be accurate for the whole tool. @handle_sb_errors def hover_action( selector: str, @@ -865,7 +962,19 @@ def hover_action( ) -@mcp.tool() +@mcp.tool( + title="Type Text", + annotations=ToolAnnotations( + # idempotent_hint is deliberately omitted: fill_input/set_value/ + # clear_only converge to the same end state on repeat calls, but + # mode="append" compounds text on each call, so no single value + # would be accurate for every mode. + read_only_hint=False, + destructive_hint=False, + open_world_hint=True, # Since "\n" can perform the "Enter" action, + # which includes form submissions. + ), +) @handle_sb_errors def type_text( selector: str, @@ -935,7 +1044,17 @@ def type_text( return f"type_text(mode={mode!r}) done for {selector}" -@mcp.tool() +@mcp.tool( + title="Select Option", + annotations=ToolAnnotations( + read_only_hint=False, + destructive_hint=False, + idempotent_hint=False, # Selecting an option can trigger arbitrary + # page-side behavior/events. + open_world_hint=True, # Selecting an option can change browser + # state and can trigger arbitrary page-side behavior. + ), +) @handle_sb_errors def select_option( dropdown_selector: str, @@ -977,7 +1096,13 @@ def select_option( return f"Selected ({by}={value!r}) in {dropdown_selector}" -@mcp.tool() +@mcp.tool( + title="Focus Element", + annotations=ToolAnnotations( + read_only_hint=False, + destructive_hint=False, + ), +) @handle_sb_errors def focus_element( selector: str, @@ -1030,7 +1155,17 @@ def focus_element( # Waiting & assertions # --------------------------------------------------------------------------- -@mcp.tool() +@mcp.tool( + title="Wait For Condition", + annotations=ToolAnnotations( + # Consistent across every state: This tool only observes/blocks, + # and it never modifies the page itself. + read_only_hint=True, + destructive_hint=False, + idempotent_hint=True, + open_world_hint=False, + ), +) @handle_sb_errors def wait_for_condition( state: Literal[ @@ -1138,7 +1273,17 @@ def wait_for_condition( return f"Element {selector} reached state '{state}'." -@mcp.tool() +@mcp.tool( + title="Assert Condition", + annotations=ToolAnnotations( + # Consistent across every check: Purely a verification/read + # operation, and never modifies the page. + read_only_hint=True, + destructive_hint=False, + idempotent_hint=True, + open_world_hint=False, + ), +) @handle_sb_errors def assert_condition( check: Literal[ @@ -1256,7 +1401,11 @@ def assert_condition( # Cookies & storage # --------------------------------------------------------------------------- -@mcp.tool() +@mcp.tool(title="Manage Cookies") +# Annotations intentionally omitted: 'get_all' is a pure read, 'clear' is +# destructive, and 'save'/'load' are file/session I/O -- no single +# read_only_hint or destructive_hint value would be accurate for the whole +# tool. @handle_sb_errors def manage_cookies( action: Literal["get_all", "clear", "save", "load"] = "get_all", @@ -1328,7 +1477,10 @@ def manage_cookies( ) -@mcp.tool() +@mcp.tool(title="Manage Storage") +# Annotations intentionally omitted: 'get' is a pure read while 'set' +# modifies storage -- no single read_only_hint value would be accurate +# for the whole tool. @handle_sb_errors def manage_storage( key: str, @@ -1404,7 +1556,20 @@ def manage_storage( # Scrolling # --------------------------------------------------------------------------- -@mcp.tool() +@mcp.tool( + title="Scroll Page", + annotations=ToolAnnotations( + # Consistent across up/down/top/bottom: a harmless, local, + # non-destructive scroll-position change. + # idempotent_hint is deliberately omitted: 'top'/'bottom' are + # idempotent (repeating lands you in the same place), but 'up'/ + # 'down' compound with each call, so no single value would be + # accurate for every direction. + read_only_hint=False, + destructive_hint=False, + open_world_hint=False, + ), +) @handle_sb_errors def scroll_page( direction: Literal["up", "down", "top", "bottom"] = "down", @@ -1460,7 +1625,10 @@ def scroll_page( # Windows & tabs # --------------------------------------------------------------------------- -@mcp.tool() +@mcp.tool(title="Manage Window") +# Annotations intentionally omitted: 'get_rect' is a pure read while +# 'set_rect'/'maximize'/'minimize' modify window state -- no single +# read_only_hint value would be accurate for the whole tool. @handle_sb_errors def manage_window( action: Literal[ @@ -1520,7 +1688,11 @@ def manage_window( ) -@mcp.tool() +@mcp.tool(title="Manage Tabs") +# Annotations intentionally omitted: 'list_tabs' is a pure read while +# 'open_new_tab'/'switch_to_tab'/'switch_to_newest_tab' modify state and +# 'close_active_tab' is destructive -- no single read_only_hint or +# destructive_hint value would be accurate for the whole tool. @handle_sb_errors def manage_tabs( action: Literal[ @@ -1621,7 +1793,16 @@ def manage_tabs( # Captcha solving # --------------------------------------------------------------------------- -@mcp.tool() +@mcp.tool( + title="Solve CAPTCHA", + annotations=ToolAnnotations( + read_only_hint=False, + destructive_hint=False, + idempotent_hint=False, # A 2nd attempt on an already-handled or + # rotated CAPTCHA widget isn't guaranteed to be a no-op. + open_world_hint=True, # Interacts with a 3rd-party CAPTCHA widget. + ), +) @handle_sb_errors def solve_captcha() -> str: """Attempt a SeleniumBase CDP-based CAPTCHA interaction, such as clicking @@ -1661,7 +1842,17 @@ def solve_captcha() -> str: # Output & misc # --------------------------------------------------------------------------- -@mcp.tool() +@mcp.tool( + title="Save Page", + annotations=ToolAnnotations( + # Consistent across screenshot/html/pdf: All three read the current + # (unmodified) page and write a new local file, which may silently + # overwrite an existing file of the same name. + read_only_hint=False, + idempotent_hint=False, + open_world_hint=False, + ), +) @handle_sb_errors def save_page( format: Literal["screenshot", "html", "pdf"] = "screenshot", @@ -1741,7 +1932,12 @@ def save_page( return f"Saved {format} as {name}" -@mcp.tool() +@mcp.tool(title="Run JavaScript") +# Annotations intentionally omitted: arbitrary JavaScript can read, write, +# navigate, or destroy data, or do nothing at all -- no annotation value +# would be more informative than the MCP spec's own conservative defaults +# for an unannotated tool (not read-only, potentially destructive, +# non-idempotent, open-world). @handle_sb_errors def run_javascript(expression: str) -> Any: """Evaluate a JavaScript expression in the current page context. From 9b0d8e97f4e3ea11a1df43a2ec6acbd64fbd50bf Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 15 Sep 2026 01:05:17 -0400 Subject: [PATCH 2/7] Update MCP Server versioning --- mcp_servers/pyproject.toml | 2 +- server.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mcp_servers/pyproject.toml b/mcp_servers/pyproject.toml index b52f0fdf255..7ba03939c94 100644 --- a/mcp_servers/pyproject.toml +++ b/mcp_servers/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "seleniumbase-mcp" -version = "1.3.5dev0" +version = "1.3.6dev0" description = "MCP server exposing SeleniumBase CDP Mode as tools for MCP clients." readme = "README.md" requires-python = ">=3.10" diff --git a/server.json b/server.json index 5fbbe4fbf8b..763cbc48c82 100644 --- a/server.json +++ b/server.json @@ -3,7 +3,7 @@ "name": "io.github.seleniumbase/seleniumbase", "title": "SeleniumBase MCP", "description": "Stealthy browser automation, testing, and web-scraping via CDP Mode.", - "version": "4.54.5", + "version": "4.54.6", "repository": { "url": "https://github.com/seleniumbase/SeleniumBase", "source": "github" @@ -13,7 +13,7 @@ { "registryType": "pypi", "identifier": "seleniumbase", - "version": "4.54.5", + "version": "4.54.6", "transport": { "type": "stdio" }, From a406b7f277de3a56dc3d83d4b5466dbca34a35e4 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 15 Sep 2026 01:06:26 -0400 Subject: [PATCH 3/7] Update the Dockerfile --- Dockerfile | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 07b721c9f4d..08c878c47f2 100755 --- a/Dockerfile +++ b/Dockerfile @@ -25,7 +25,7 @@ RUN locale-gen en_US.UTF-8 # Fingerprint Configuration #=========================== RUN apt-get update -RUN apt install -y fonts-liberation fonts-noto-color-emoji libvulkan1 libnss3 libatk-bridge2.0-0 libcups2 libxcomposite1 libxrandr2 libgbm1 libpango-1.0-0 libcairo2 +RUN apt install -y fonts-noto-color-emoji libvulkan1 libnss3 libatk-bridge2.0-0 libcups2 libxcomposite1 libxrandr2 libgbm1 libpango-1.0-0 libcairo2 RUN apt install -y fonts-freefont-ttf fonts-dejavu-core fonts-ubuntu fonts-roboto fonts-droid-fallback #====================== @@ -33,6 +33,7 @@ RUN apt install -y fonts-freefont-ttf fonts-dejavu-core fonts-ubuntu fonts-robot #====================== RUN apt-get update RUN apt-get install -y \ + fonts-clear-sans \ fonts-liberation2 \ fonts-font-awesome \ fonts-terminus \ @@ -125,7 +126,16 @@ COPY MANIFEST.in /SeleniumBase/MANIFEST.in COPY pytest.ini /SeleniumBase/pytest.ini COPY setup.cfg /SeleniumBase/setup.cfg COPY virtualenv_install.sh /SeleniumBase/virtualenv_install.sh +COPY mcp_servers /SeleniumBase/mcp_servers/ +COPY README.md /SeleniumBase/README.md +COPY pyproject.toml /SeleniumBase/pyproject.toml +COPY LICENSE /SeleniumBase/LICENSE RUN find . -name '*.pyc' -delete +RUN echo '#!/bin/sh' > /usr/local/bin/pip \ + && echo 'exec python3.13 -m pip "$@"' >> /usr/local/bin/pip \ + && chmod +x /usr/local/bin/pip \ + && pip --version +ENV PIP_BREAK_SYSTEM_PACKAGES=1 RUN pip install --upgrade pip setuptools wheel RUN cd /SeleniumBase && ls && pip install -r requirements.txt --upgrade RUN cd /SeleniumBase && pip install . From 8c9805f870bd1a2a0bb9b8ef79e710848a9e81f2 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 15 Sep 2026 01:08:12 -0400 Subject: [PATCH 4/7] Update CDP Mode --- seleniumbase/undetected/cdp_driver/tab.py | 30 ++++++++++++++--------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/seleniumbase/undetected/cdp_driver/tab.py b/seleniumbase/undetected/cdp_driver/tab.py index 82628c42e84..e268ccf87bf 100644 --- a/seleniumbase/undetected/cdp_driver/tab.py +++ b/seleniumbase/undetected/cdp_driver/tab.py @@ -222,14 +222,15 @@ async def find( except (Exception, TypeError): pass while not item: - item = await self.find_element_by_text( - text, best_match, return_enclosing_element - ) if loop.time() - start_time > timeout: raise asyncio.TimeoutError( "Time ran out while waiting for: {%s}" % text ) + await self await self.sleep(0.5) + item = await self.find_element_by_text( + text, best_match, return_enclosing_element + ) return item async def select( @@ -273,13 +274,13 @@ async def find_all( except (Exception, TypeError): pass while not items: - await self - items = await self.find_elements_by_text(text) if loop.time() - now > timeout: raise asyncio.TimeoutError( "Time ran out while waiting for: {%s}" % text ) + await self await self.sleep(0.5) + items = await self.find_elements_by_text(text) return items async def select_all( @@ -311,13 +312,13 @@ async def select_all( items.extend(await fr.query_selector_all(selector)) items.extend(await self.query_selector_all(selector)) while not items: - await self - items = await self.query_selector_all(selector) if loop.time() - now > timeout: raise asyncio.TimeoutError( "Time ran out while waiting for: {%s}" % selector ) + await self await self.sleep(0.5) + items = await self.query_selector_all(selector) return items async def get( @@ -1098,25 +1099,32 @@ async def wait_for( """ loop = asyncio.get_running_loop() now = loop.time() + item = None if selector: - item = await self.query_selector(selector) - while not item: + with suppress(Exception): item = await self.query_selector(selector) + while not item: if loop.time() - now > timeout: raise asyncio.TimeoutError( "Time ran out while waiting for: {%s}" % selector ) + await self await self.sleep(0.068) + with suppress(Exception): + item = await self.query_selector(selector) return item if text: - item = await self.find_element_by_text(text) - while not item: + with suppress(Exception): item = await self.find_element_by_text(text) + while not item: if loop.time() - now > timeout: raise asyncio.TimeoutError( "Time ran out while waiting for: {%s}" % text ) + await self await self.sleep(0.068) + with suppress(Exception): + item = await self.find_element_by_text(text) return item async def set_attributes(self, selector, attribute, value): From 7f488faed38e8afce01d18b30291906e75dfe72d Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 15 Sep 2026 01:08:36 -0400 Subject: [PATCH 5/7] Update `uv` --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9c829b2ccb4..f89c306f298 100755 --- a/setup.py +++ b/setup.py @@ -303,7 +303,7 @@ # Required for local MCP server debugging with: # mcp dev server.py "uv": [ - "uv>=0.12.12" + "uv>=0.12.14" ], }, packages=[ From b4910211a8e801eebab9f0b1a6679bf1077ac53f Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 15 Sep 2026 01:09:08 -0400 Subject: [PATCH 6/7] Update CDP Mode examples --- examples/cdp_mode/raw_cdp_downloads.py | 2 +- examples/cdp_mode/raw_indeed_jobs.py | 1 - examples/cdp_mode/raw_mouser.py | 4 ++-- examples/cdp_mode/raw_softpedia.py | 1 - examples/cdp_mode/raw_wsform.py | 8 +++----- 5 files changed, 6 insertions(+), 10 deletions(-) diff --git a/examples/cdp_mode/raw_cdp_downloads.py b/examples/cdp_mode/raw_cdp_downloads.py index 1c0612c1012..87fe4b95870 100644 --- a/examples/cdp_mode/raw_cdp_downloads.py +++ b/examples/cdp_mode/raw_cdp_downloads.py @@ -26,7 +26,7 @@ sb.assert_element("span#pip-command") sb.assert_text("Download files", "div#files h2.page-title") sb.assert_text("Download files", "a#files-tab") -pkg_header = sb.get_text("h1.package-header__name").strip() +pkg_header = sb.get_text('h1[class*="header__name"]').strip() pkg_name = pkg_header.replace(" ", "-") whl_file = pkg_name + "-py3-none-any.whl" tar_gz_file = pkg_name + ".tar.gz" diff --git a/examples/cdp_mode/raw_indeed_jobs.py b/examples/cdp_mode/raw_indeed_jobs.py index 2529cf38eea..d3fb8edd51a 100644 --- a/examples/cdp_mode/raw_indeed_jobs.py +++ b/examples/cdp_mode/raw_indeed_jobs.py @@ -12,5 +12,4 @@ for i, item in enumerate(items): print(f"* <====== {i + 1} ======>") print(item.text) - item.scroll_into_view() print(f"*** {len(items)} total items found!") diff --git a/examples/cdp_mode/raw_mouser.py b/examples/cdp_mode/raw_mouser.py index 0f88b404235..8cece98efd5 100644 --- a/examples/cdp_mode/raw_mouser.py +++ b/examples/cdp_mode/raw_mouser.py @@ -3,7 +3,7 @@ with SB(uc=True, test=True, guest=True) as sb: sb.activate_cdp_mode() sb.goto("https://www.mouser.com/") - search_box = 'input[name="keyword"]' + search_box = 'input[data-testid="global-search"]' sb.sleep(1.6) sb.solve_captcha() sb.sleep(1.8) @@ -11,7 +11,7 @@ sb.sleep(1.2) sb.press_keys(search_box, "FLUKE-TC01B 25HZ") sb.sleep(1.2) - sb.click('button[type="submit"]') + sb.click('a[id*="search-option"]') sb.sleep(3.2) sb.wait_for_element("span#spnDescription") soup = sb.get_beautiful_soup() diff --git a/examples/cdp_mode/raw_softpedia.py b/examples/cdp_mode/raw_softpedia.py index 4d37c5c40a1..62f9f14192c 100644 --- a/examples/cdp_mode/raw_softpedia.py +++ b/examples/cdp_mode/raw_softpedia.py @@ -23,4 +23,3 @@ for link in links: sb.goto(link) sb.remove_elements("div.ad") - sb.sleep(2) diff --git a/examples/cdp_mode/raw_wsform.py b/examples/cdp_mode/raw_wsform.py index 387dba7f8ef..04002455491 100644 --- a/examples/cdp_mode/raw_wsform.py +++ b/examples/cdp_mode/raw_wsform.py @@ -1,12 +1,10 @@ -"""CDP Mode for bypassing bot-detection & CAPTCHAs. -Note: sb.uc_gui_click_captcha() requires PyAutoGUI, -which is installed automatically if not already.""" +"""CDP Mode for bypassing bot-detection & CAPTCHAs.""" from seleniumbase import SB with SB(uc=True, test=True, locale="en", incognito=True) as sb: sb.activate_cdp_mode() sb.goto("https://wsform.com/demo/") sb.sleep(2) - sb.scroll_into_view("div.grid") - sb.uc_gui_click_captcha() # PyAutoGUI mouse click + sb.scroll_into_view('form[method="POST"]') + sb.solve_captcha() sb.sleep(2) From 305df28d32b614592733b9771e3e11a8135f0f15 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Tue, 15 Sep 2026 01:09:23 -0400 Subject: [PATCH 7/7] Version 4.54.6 --- seleniumbase/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py index cc91c81bb3f..92d0477e38d 100755 --- a/seleniumbase/__version__.py +++ b/seleniumbase/__version__.py @@ -1,2 +1,2 @@ # seleniumbase package -__version__ = "4.54.5" +__version__ = "4.54.6"