diff --git a/examples/cdp_mode/ReadMe.md b/examples/cdp_mode/ReadMe.md index 4c7f0601002..39827d68c6c 100644 --- a/examples/cdp_mode/ReadMe.md +++ b/examples/cdp_mode/ReadMe.md @@ -401,7 +401,7 @@ sb.tile_windows(windows=None, max_columns=0) sb.grant_permissions(permissions, origin=None) sb.grant_all_permissions() sb.reset_permissions() -sb.get_all_urls(absolute=True) +sb.get_all_urls(absolute=True, selector=None, timeout=None) sb.get_all_cookies(*args, **kwargs) sb.set_all_cookies(*args, **kwargs) sb.save_cookies(*args, **kwargs) @@ -477,8 +477,8 @@ sb.get_gui_element_rect(selector, timeout=None) sb.get_gui_element_center(selector, timeout=None) sb.get_document() sb.get_flattened_document() -sb.get_element_attributes(selector) -sb.get_element_attribute(selector, attribute) +sb.get_element_attributes(selector, timeout=None) +sb.get_element_attribute(selector, attribute, timeout=None) sb.get_attribute(selector, attribute) sb.get_element_html(selector, timeout=None) sb.get_mfa_code(totp_key=None) @@ -557,7 +557,7 @@ sb.assert_not_equal(first, second) sb.assert_in(first, second) sb.assert_not_in(first, second) sb.js_scroll_into_view(selector) -sb.scroll_into_view(selector) +sb.scroll_into_view(selector, timeout=None) sb.scroll_to_y(y) sb.scroll_to_top() sb.scroll_to_bottom() diff --git a/help_docs/cdp_mode_methods.md b/help_docs/cdp_mode_methods.md index 9976faed449..f800533ea9e 100644 --- a/help_docs/cdp_mode_methods.md +++ b/help_docs/cdp_mode_methods.md @@ -47,7 +47,7 @@ sb.tile_windows(windows=None, max_columns=0) sb.grant_permissions(permissions, origin=None) sb.grant_all_permissions() sb.reset_permissions() -sb.get_all_urls(absolute=True, selector=None) +sb.get_all_urls(absolute=True, selector=None, timeout=None) sb.get_all_cookies(*args, **kwargs) sb.set_all_cookies(*args, **kwargs) sb.save_cookies(*args, **kwargs) @@ -123,8 +123,8 @@ sb.get_gui_element_rect(selector, timeout=None) sb.get_gui_element_center(selector, timeout=None) sb.get_document() sb.get_flattened_document() -sb.get_element_attributes(selector) -sb.get_element_attribute(selector, attribute) +sb.get_element_attributes(selector, timeout=None) +sb.get_element_attribute(selector, attribute, timeout=None) sb.get_attribute(selector, attribute) sb.get_element_html(selector, timeout=None) sb.get_mfa_code(totp_key=None) @@ -202,7 +202,7 @@ sb.assert_equal(first, second) sb.assert_not_equal(first, second) sb.assert_in(first, second) sb.assert_not_in(first, second) -sb.js_scroll_into_view(selector) +sb.js_scroll_into_view(selector, timeout=None) sb.scroll_into_view(selector) sb.scroll_to_y(y) sb.scroll_to_top() diff --git a/mcp_servers/README.md b/mcp_servers/README.md index 7ce6d94daf4..401a4709855 100644 --- a/mcp_servers/README.md +++ b/mcp_servers/README.md @@ -146,23 +146,31 @@ 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 (including `click`, when not using `all_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: `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. +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, include_shadow_dom)`, `get_attributes`, `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)` | +| 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`, | +| Output & misc | `save_output(format: screenshot/html/pdf)`, `run_javascript` | ## Design notes / things to adapt for your use case @@ -178,19 +186,25 @@ Tools here are grouped around a shared `selector` convention: `selector` args ac - **Navigation and history live in one tool: `manage_history`.** What used to be `navigate_history` is now `manage_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 has `id`, `url`, `user_typed_url`, `title`, and `transition_type`. `"back"`, `"forward"`, and `"reload"` behave as before. This is the only way to retrieve navigation history now — `get_page_info` doesn't return it. -- **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`. +- **`get_content` always reads from an element, not the whole document.** `selector` now defaults to `"body"` rather than `None`/whole-page, and there's no `include_shadow_dom` option anymore — `get_content` no longer calls `get_page_source` at all. `output_format="html"` returns a single element's outer HTML (`get_element_html`), and `output_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_content` also gained a `timeout` parameter (default 5s) for waiting on the target element. + +- **`get_attributes` and `focus` now take a `timeout`.** 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_condition` is deliberately narrow.** Its `check` parameter only accepts `"present"` or `"visible"` — there's no built-in `"count"` check anymore; call `find_elements` and read the returned `count` field instead. Passing `text` checks whether that text is visible within `selector` and takes priority over `check` when both are given — so `check_condition(text="Sign in")` behaves differently from `check_condition(check="visible")`, not as two variants of the same check. Note that an empty string for `text` (or for `wait_for`'s `selector`/`text`) is treated as not provided, since both tools now branch on truthiness rather than on `is not None`. -- **`find_elements` defaults to a fast, non-raising lookup.** Its default `timeout` is 0.5 seconds (not 5, unlike most other tools here), and a failed lookup returns `{"count": 0, "matches": []}` instead of raising — there is no error string on a miss, just an empty result. Pass a longer `timeout` explicitly if the elements you're looking for may still be loading. +- **`find_elements` catches its own lookup failures.** Its default `timeout` is 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": "
"}` — the error lives inside the returned dict rather than surfacing as a top-level string from `handle_sb_errors`. Pass a longer `timeout` explicitly if the elements you're looking for may still be loading. + +- **`wait_seconds` was folded into `wait_for`.** There's no standalone `wait_seconds` tool anymore. Use `wait_for(state="seconds_passed", timeout=)` instead — it ignores `selector`/`text` and blocks for the full `timeout` duration. All other `wait_for` states behave as before. + +- **Hover, click-after-hover, and drag-and-drop share one tool.** `hover_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"`.) -- **Hover, clicking after hover, and drag-and-drop share one tool.** `hover_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`.** What used to be `act_on_element` is now `focus(selector, action, timeout)`, with actions `scroll_to_element` (the default), `focus`, and `highlight` — 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; use `click`, `type_text`, `select_option`, or `hover_action` for that. -- **Non-activating element actions are `focus`.** What used to be `act_on_element` is now `focus(selector, action)`, with actions `scroll_to_element` (the default), `focus`, and `highlight` — 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; use `click`, `type_text`, `select_option`, or `hover_action` for that. +- **`scroll`'s `amount` isn't capped at 100.** Relative up/down scrolling by more than 100% of the viewport height is allowed (e.g. `amount=200` scrolls 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, 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). +- **CAPTCHA-solving.** `solve_captcha` attempts 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_javascript` runs arbitrary JS, and `manage_storage` can expose authentication/session secrets; `manage_cookies` and `save_output` accept 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. diff --git a/mcp_servers/pyproject.toml b/mcp_servers/pyproject.toml index c568056bcc1..1369694b0f6 100644 --- a/mcp_servers/pyproject.toml +++ b/mcp_servers/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "seleniumbase-mcp" -version = "1.3.1dev0" +version = "1.3.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 25f92ea0d3b..4b5e6c5bf19 100644 --- a/mcp_servers/server.py +++ b/mcp_servers/server.py @@ -446,10 +446,16 @@ def find_elements( dictionaries. It does not return live SeleniumBase element objects. Args: - selector: A SeleniumBase-supported selector, typically CSS or XPath. + selector: A CSS selector, or an XPath selector that SeleniumBase can + convert to CSS. In sb.find_elements, SeleniumBase automatically + attempts to convert XPath to CSS. Some XPath expressions, such + as those using `contains(...)`, cannot be converted to CSS and + therefore aren't supported by this tool. timeout: Maximum number of seconds to wait for at least one matching - element to appear. + element to appear. If the selector is an XPath selector that + cannot be converted into a valid CSS selector, then the wait + might be less than the timeout. include_html: If True, include each matching element's outer HTML. If False, return only tag name and text. @@ -459,7 +465,8 @@ def find_elements( - count: Number of matching elements found. - matches: A list of element dictionaries containing tag_name and text, plus html when include_html=True. - If there's an error, returns a string with error details. + If there's an error during search, then "error" is added into the + returned dictionary with error details. Tool selection: - Need structured information about matching elements -> @@ -470,77 +477,63 @@ def find_elements( - Need to know whether an element is present/visible -> use check_condition. - Note: + Notes: Element handles cannot be persisted across MCP calls. If you find elements and then need to act on one, resolve it again with the appropriate interaction tool. + + For uncaught errors, @handle_sb_errors returns strings. """ sb = _get_sb() - elements = sb.find_elements(selector, timeout=timeout) - - if include_html: + try: + elements = sb.find_elements(selector, timeout=timeout) + except Exception as e: return { - "count": len(elements), - "matches": [ - { - "tag_name": element.tag_name, - "text": element.text, - "html": element.get_html(), - } - for element in elements - ], + "count": 0, + "matches": [], + "error": str(e), } + matches = [] + for element in elements: + match = { + "tag_name": element.tag_name, + "text": element.text, + } + if include_html: + match["html"] = element.get_html() + matches.append(match) + return { - "count": len(elements), - "matches": [ - { - "tag_name": element.tag_name, - "text": element.text, - } - for element in elements - ], + "count": len(matches), + "matches": matches, } @mcp.tool() @handle_sb_errors def get_content( - selector: str | None = None, + selector: str = "body", output_format: Literal["text", "html", "urls"] = "text", - include_shadow_dom: bool = True, + timeout: float = 5, ) -> str | list[str]: - """Read visible text, HTML, or discovered URLs from the current page. + """Read visible text, HTML, or discovered URLs from the selected element. - Use this tool when you need actual page content or URL information rather - than page metadata. + Use this tool when you need to get actual page content or URL information + rather than page metadata. Args: - selector: Optional CSS selector or SeleniumBase text-matching selector - identifying the element whose content should be read. For - output_format="text" or "html", the selector scopes the returned - content to that element. For output_format="urls", the selector - scopes URL discovery to URLs within that element. When omitted, - the operation applies to the whole page. + selector: CSS selector or SeleniumBase-supported XPath selector. output_format: - - "text": Return visible text from the page or selected element. - - "html": Return HTML from the page or selected element. - - "urls": Return URLs discovered by SeleniumBase from the page - or selected element. URLs associated with elements such as - anchors, links, images, scripts, and metadata may be included. - SeleniumBase returns full URLs with their URL protocol prefixes. + - "text": Return visible text from the selected element. + - "html": Return HTML from the selected element. + - "urls": Return URLs discovered by SeleniumBase within the + selected element. Returned URLs are normalized to full URLs + with their protocol prefixes. - include_shadow_dom: When output_format="html" and selector is omitted, - include any shadow-root HTML present in the page. This option has - no effect for "text" or "urls", or when a selector is specified. - Returns: - For output_format="text", a string containing visible text. - For output_format="html", a string containing HTML. - For output_format="urls", a list of URL strings. This is useful for - crawling, link discovery, resource inspection, and finding candidate - URLs before navigating to them. + timeout: Maximum seconds to wait for the target element. Default: 5. Tool selection: - Need URL, title, origin, or User-Agent -> use get_page_info. @@ -554,20 +547,19 @@ def get_content( """ sb = _get_sb() - if output_format == "urls": - return sb.get_all_urls(selector=selector) - - if selector is None: - if output_format == "html": - return sb.get_page_source( - include_shadow_dom=include_shadow_dom - ) - return sb.get_text("body") + if output_format == "text": + return sb.get_text(selector, timeout=timeout) if output_format == "html": - return sb.get_element_html(selector) + return sb.get_element_html(selector, timeout=timeout) - return sb.get_text(selector) + if output_format == "urls": + return sb.get_all_urls(selector=selector, timeout=timeout) + + return ( + f"Error: unknown output_format '{output_format}'. " + "Use 'text', 'html', or 'urls'." + ) @mcp.tool() @@ -575,22 +567,24 @@ def get_content( def get_attributes( selector: str, attribute: str | None = None, -) -> Any: - """Read HTML attributes from a matching element. + 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 one or more HTML attributes - such as href, src, value, class, id, name, type, aria-label, or data-*. + 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. Args: - selector: CSS selector or SeleniumBase text-matching selector for - the target element. + selector: CSS selector or SeleniumBase-supported XPath selector. attribute: Specific HTML attribute to retrieve. When omitted, return - all HTML attributes of the element as a dictionary. + all HTML attributes of the first matching element as a dictionary. + + timeout: Maximum seconds to wait for the target element. Default: 5. Returns: - The requested attribute value, or a dictionary containing all - HTML attributes of the element when attribute is omitted. + The requested attribute(s). Tool selection: - Need one or more HTML attribute values from a specific element -> @@ -601,13 +595,16 @@ def get_attributes( - Need to check element presence/visibility -> use 'check_condition'. This is a read-only operation. + + If there's no matching element found within the timeout, + then @handle_sb_errors will return details from the exception raised. """ sb = _get_sb() if attribute: - return sb.get_attribute(selector, attribute) + return sb.get_attribute(selector, attribute, timeout=timeout) - return sb.get_element_attributes(selector) + return sb.get_element_attributes(selector, timeout=timeout) @mcp.tool() @@ -963,6 +960,7 @@ def focus( "focus", "highlight", ] = "scroll_to_element", + timeout: float = 5, ) -> str: """Scroll to, focus, or highlight an element. @@ -976,16 +974,22 @@ def focus( - "scroll_to_element": Scroll the element into the viewport. - "focus": Move keyboard focus to the element. - "highlight": Temporarily highlight the element for debugging or - demonstration. May affect timing and reduce stealth. + demonstration by changing the border color. May affect timing + and/or reduce stealth. + + timeout: Maximum seconds to wait for the target element. Default: 5. + + If there's no matching element found within the timeout, + then @handle_sb_errors will return details from the exception raised. """ sb = _get_sb() if action == "scroll_to_element": - sb.scroll_into_view(selector) + sb.scroll_into_view(selector, timeout=timeout) elif action == "focus": - sb.find_element(selector).focus() + sb.find_element(selector, timeout=timeout).focus() elif action == "highlight": - sb.highlight(selector) + sb.highlight(selector, timeout=timeout) else: return ( f"Error: unknown action '{action}'. " diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py index 2868a9ea493..e4b91124a34 100755 --- a/seleniumbase/__version__.py +++ b/seleniumbase/__version__.py @@ -1,2 +1,2 @@ # seleniumbase package -__version__ = "4.54.1" +__version__ = "4.54.2" diff --git a/seleniumbase/core/sb_cdp.py b/seleniumbase/core/sb_cdp.py index 7aea9e77a31..8974d7902b9 100644 --- a/seleniumbase/core/sb_cdp.py +++ b/seleniumbase/core/sb_cdp.py @@ -827,7 +827,7 @@ def reset_permissions(self): driver = driver.cdp_base return self.loop.run_until_complete(driver.reset_permissions()) - def get_all_urls(self, absolute=True, selector=None): + def get_all_urls(self, absolute=True, selector=None, timeout=None): """ Convenience function that returns all links (a,link,img,script). :param absolute: @@ -838,13 +838,15 @@ def get_all_urls(self, absolute=True, selector=None): instead of the entire page. :return: List of URLs. """ + if not timeout: + timeout = settings.SMALL_TIMEOUT if isinstance(absolute, str) and selector is None: # The `selector` is usually first, so the user may get confused. # Therefore, if first arg is string, it's probably the selector. selector = absolute absolute = True if selector: - html = self.find_element(selector).get_html() + html = self.find_element(selector, timeout=timeout).get_html() soup = self.get_beautiful_soup(html) page_url = self.get_current_url() return page_utils._get_unique_links(page_url, soup) @@ -1876,8 +1878,11 @@ def get_document(self): def get_flattened_document(self): return self.loop.run_until_complete(self.page.get_flattened_document()) - def get_element_attributes(self, selector): + def get_element_attributes(self, selector, timeout=None): + if not timeout: + timeout = settings.SMALL_TIMEOUT selector = self.__convert_to_css_if_xpath(selector) + self.find_element(selector, timeout=timeout) return self.loop.run_until_complete( self.page.js_dumps( """document.querySelector('%s')""" @@ -1885,10 +1890,13 @@ def get_element_attributes(self, selector): ) ) - def get_element_attribute(self, selector, attribute): + def get_element_attribute(self, selector, attribute, timeout=None): """Find an element and return the value of an attribute. Raises an exception if there's no such element or attribute.""" - attributes = self.get_element_attributes(selector) + if not timeout: + timeout = settings.SMALL_TIMEOUT + selector = self.__convert_to_css_if_xpath(selector) + attributes = self.get_element_attributes(selector, timeout=timeout) with suppress(Exception): return attributes[attribute] locate = ' %s="' % attribute @@ -3685,8 +3693,10 @@ def js_scroll_into_view(self, selector): with suppress(Exception): self.loop.run_until_complete(self.page.evaluate(js_code)) - def scroll_into_view(self, selector): - self.find_element(selector).scroll_into_view() + def scroll_into_view(self, selector, timeout=None): + if not timeout: + timeout = settings.SMALL_TIMEOUT + self.find_element(selector, timeout=timeout).scroll_into_view() self.loop.run_until_complete(self.page.wait(0.05)) def scroll_to_y(self, y): diff --git a/server.json b/server.json index 825cf81d771..3192631c4f5 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.1", + "version": "4.54.2", "repository": { "url": "https://github.com/seleniumbase/SeleniumBase", "source": "github" @@ -13,7 +13,7 @@ { "registryType": "pypi", "identifier": "seleniumbase", - "version": "4.54.1", + "version": "4.54.2", "transport": { "type": "stdio" },