From b4c91e208b78ea6e65b616e63506bdbd7b39dba5 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 2 Sep 2026 00:51:41 -0400 Subject: [PATCH 1/5] Loosen the restrictions when validating proxy strings --- seleniumbase/core/proxy_helper.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/seleniumbase/core/proxy_helper.py b/seleniumbase/core/proxy_helper.py index 7c3c06ecc99..2af7e0a8d22 100644 --- a/seleniumbase/core/proxy_helper.py +++ b/seleniumbase/core/proxy_helper.py @@ -185,6 +185,9 @@ def validate_proxy_string(proxy_string, keep_scheme=False): val_ip = re.match( r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+$", proxy_string ) + HOSTNAME_PATTERN = ( + re.compile(r"^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?$") + ) if not val_ip: if proxy_string.startswith("http://"): proxy_string = proxy_string.split("http://")[1] @@ -200,7 +203,10 @@ def validate_proxy_string(proxy_string, keep_scheme=False): chunks = proxy_string.split(":") if len(chunks) == 2: if re.match(r"^\d+$", chunks[1]): - if page_utils.is_valid_url("http://" + proxy_string): + if ( + page_utils.is_valid_url("http://" + proxy_string) + or HOSTNAME_PATTERN.match(chunks[0]) + ): valid = True elif len(chunks) == 3: if re.match(r"^\d+$", chunks[2]): From 006829363354b49dfb87ec7e1722e3b41a41ca10 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 2 Sep 2026 00:52:21 -0400 Subject: [PATCH 2/5] Update CDP Mode --- examples/cdp_mode/ReadMe.md | 1 + help_docs/cdp_mode_methods.md | 1 + seleniumbase/core/browser_launcher.py | 1 + seleniumbase/core/sb_cdp.py | 23 ++++++++++++++++++++++- 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/examples/cdp_mode/ReadMe.md b/examples/cdp_mode/ReadMe.md index 06afe162227..4c7f0601002 100644 --- a/examples/cdp_mode/ReadMe.md +++ b/examples/cdp_mode/ReadMe.md @@ -428,6 +428,7 @@ sb.remove_element(selector) sb.remove_from_dom(selector) sb.remove_elements(selector) sb.send_keys(selector, text, timeout=None) +sb.fast_keys(selector, text, timeout=None) sb.press_keys(selector, text, timeout=None) sb.type(selector, text, timeout=None) sb.fast_type(selector, text, timeout=None) diff --git a/help_docs/cdp_mode_methods.md b/help_docs/cdp_mode_methods.md index 2b1b2e51ca8..9976faed449 100644 --- a/help_docs/cdp_mode_methods.md +++ b/help_docs/cdp_mode_methods.md @@ -74,6 +74,7 @@ sb.remove_element(selector) sb.remove_from_dom(selector) sb.remove_elements(selector) sb.send_keys(selector, text, timeout=None) +sb.fast_keys(selector, text, timeout=None) sb.press_keys(selector, text, timeout=None) sb.type(selector, text, timeout=None) sb.fast_type(selector, text, timeout=None) diff --git a/seleniumbase/core/browser_launcher.py b/seleniumbase/core/browser_launcher.py index 1b3fde685cf..27ef7d7078c 100644 --- a/seleniumbase/core/browser_launcher.py +++ b/seleniumbase/core/browser_launcher.py @@ -821,6 +821,7 @@ def uc_open_with_cdp_mode(driver, url=None, **kwargs): cdp.remove_from_dom = CDPM.remove_from_dom cdp.remove_elements = CDPM.remove_elements cdp.send_keys = CDPM.send_keys + cdp.fast_keys = CDPM.fast_keys cdp.press_keys = CDPM.press_keys cdp.type = CDPM.type cdp.fast_type = CDPM.fast_type diff --git a/seleniumbase/core/sb_cdp.py b/seleniumbase/core/sb_cdp.py index 308ce7fb3f0..7aea9e77a31 100644 --- a/seleniumbase/core/sb_cdp.py +++ b/seleniumbase/core/sb_cdp.py @@ -1280,6 +1280,26 @@ def send_keys(self, selector, text, timeout=None): self.__slow_mode_pause_if_set() self.loop.run_until_complete(self.page.sleep(0.025)) + def fast_keys(self, selector, text, timeout=None): + """Similar to send_keys(), but presses keys at full speed. + This method DOES NOT clear the text field before pressing.""" + if not timeout: + timeout = settings.SMALL_TIMEOUT + self.__slow_mode_pause_if_set() + element = self.select(selector, timeout=timeout) + element.scroll_into_view() + if text.endswith("\n") or text.endswith("\r"): + text = text[:-1] + "\r\n" + elif ( + element.tag_name == "textarea" + and "\n" in text + and "\r" not in text + ): + text = text.replace("\n", "\r") + element.send_keys(text, fast=True) + self.__slow_mode_pause_if_set() + self.loop.run_until_complete(self.page.sleep(0.025)) + def press_keys(self, selector, text, timeout=None): """Similar to send_keys(), but presses keys at human speed.""" if not timeout: @@ -1313,7 +1333,8 @@ def type(self, selector, text, timeout=None): self.loop.run_until_complete(self.page.sleep(0.025)) def fast_type(self, selector, text, timeout=None): - """Similar to send_keys(), but presses keys really fast. + """Similar to type(), but presses keys at full speed. + This method clears the text field before typing. (Don't use if going for stealth. This is just for speed.)""" if not timeout: timeout = settings.SMALL_TIMEOUT From c8e6ce64189959eb27b7a2702f0013abd4e35d2a Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 2 Sep 2026 01:27:25 -0400 Subject: [PATCH 3/5] Update the MCP Server --- mcp_servers/README.md | 10 ++-- mcp_servers/pyproject.toml | 2 +- mcp_servers/server.py | 117 +++++++++++++++++++++---------------- 3 files changed, 73 insertions(+), 56 deletions(-) diff --git a/mcp_servers/README.md b/mcp_servers/README.md index 6d168043c73..13987eac43a 100644 --- a/mcp_servers/README.md +++ b/mcp_servers/README.md @@ -100,11 +100,11 @@ Restart Claude Desktop. You should see a 🔨 tools icon indicating the server c * `check_state` * `click` * `hover_with_action` -* `fill_input` +* `type_text` * `select_option` * `focus_on` * `wait_for` -* `assert_that` +* `assert_condition` * `manage_cookies` * `manage_storage` * `scroll` @@ -156,9 +156,9 @@ Tools here are grouped around a shared `selector` convention: `selector` args ac | 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_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)` | +| Interacting | `click(selector, nth, all_matches, only_if_visible, parent_selector, timeout, scroll)`, `hover_with_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_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)` | +| 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)` | @@ -179,7 +179,7 @@ Tools here are grouped around a shared `selector` convention: `selector` args ac - **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. +- **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`, `type_text`, `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. diff --git a/mcp_servers/pyproject.toml b/mcp_servers/pyproject.toml index 739bc1f57b9..6a8c49a8b8e 100644 --- a/mcp_servers/pyproject.toml +++ b/mcp_servers/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "seleniumbase-mcp" -version = "1.2.2dev0" +version = "1.2.3dev0" 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 8f794588645..1e4319e1b0d 100644 --- a/mcp_servers/server.py +++ b/mcp_servers/server.py @@ -30,9 +30,9 @@ elements as structured data. - Use check_state for an immediate, non-waiting state check. - 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_with_action/focus_on for +- Use assert_condition when the agent needs to verify an expected condition + and treat failure as an assertion error. +- Use click/type_text/select_option/hover_with_action/focus_on for interactions and element positioning. """ from __future__ import annotations @@ -93,7 +93,7 @@ def start_browser( """Launch a persistent SeleniumBase Pure CDP Mode browser session. This must be called before browser interaction tools such as navigate, - get_content, click, fill_input, or find_elements. The same browser + get_content, click, type_text, or find_elements. The same browser session remains active across subsequent MCP tool calls until close_browser is called or the server process exits. @@ -277,7 +277,7 @@ def get_page_info() -> dict | str: - Need information about matching elements -> use find_elements. - Need an immediate state check -> use check_state. - Need to wait for a condition -> use wait_for. - - Need to verify an expected condition -> use assert_that. + - Need to verify an expected condition -> use assert_condition. Unlike a dedicated browser-status tool, get_page_info is the single source of browser/page metadata. If no browser session is active, it @@ -549,15 +549,26 @@ def get_attributes( ) -> Any: """Read HTML attributes from a 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-*. + Args: selector: CSS selector or SeleniumBase text-matching selector for the target element. attribute: Specific HTML attribute to retrieve. When omitted, return - all available attributes as a dictionary. + all HTML attributes of the element as a dictionary. Returns: The requested attribute value, or a dictionary containing all - attributes when attribute is omitted. + HTML attributes of the element when attribute is omitted. + + Tool selection: + - Need one or more HTML attribute values from a specific element -> + use this tool. + - Need to discover multiple matching elements or inspect their text -> + use 'find_elements'. + - Need visible text or HTML content -> use 'get_content'. + - Need to check presence or visibility -> use 'check_state'. This is a read-only operation and does not modify the element. """ @@ -580,7 +591,7 @@ def check_state( Use this tool when you need an observation of the current state and do NOT want to wait for a condition. For waiting behavior, use wait_for. - For an expectation that should fail as an assertion, use assert_that. + For an expectation that should fail as an assertion, use assert_condition. Args: check: @@ -602,7 +613,7 @@ def check_state( - Immediate yes/no/count observation -> use check_state. - Wait until a state becomes true/false -> use wait_for. - Verify an expected condition and fail when it is not met -> - use assert_that. + use assert_condition. Note: Except for count's short lookup, this tool does not wait for elements @@ -784,62 +795,64 @@ def hover_with_action( @mcp.tool() @handle_sb_errors -def fill_input( +def type_text( selector: str, text: str = "", mode: Literal[ - "type", + "fill_input", "append", - "set_value", "fast_type", - "clear", - ] = "type", + "set_value", + "clear_only", + ] = "fill_input", timeout: int | float | None = 7, ) -> str: - """Enter, append, directly set, or clear text in a form control. + """Fill, append, fast-type, directly set, or clear a form control. Use this tool for input elements, textareas, and contenteditable elements. Args: selector: CSS selector or SeleniumBase selector identifying the input, textarea, or contenteditable element. - text: Text to enter or set. Ignored when mode="clear". + text: Text to enter or set. Not used when mode="clear_only". mode: - - "type": Clear the field and type text normally. - - "append": Keep the existing value and send text as keystrokes. + - "fill_input": Clear the field and then type text normally. + - "append": Keep the existing value and add text as keystrokes. + - "fast_type": Clear the field and type text without pauses. - "set_value": Set the value directly and immediately. This can be useful for fast form filling but does not simulate normal - key events. - - "fast_type": Clear the field and type text quickly. - - "clear": Empty the field; text is ignored. + key events. It can also be used to handle input sliders, + e.g. 'input[type="range"]'. + - "clear_only": Empty the text field; text is ignored. timeout: Maximum seconds to wait for the target element. Tool selection: - - Normal human-like text entry -> mode="type". - - Add text without clearing -> mode="append". - - Directly set a value -> mode="set_value". - - Fast typing -> mode="fast_type". - - Empty a field -> mode="clear". + - Normal text entry to replace existing text -> mode="fill_input". + - Add text without clearing the field first -> mode="append". + - Fast typing to replace existing text -> mode="fast_type". + - Directly set a value (e.g. input slider) -> mode="set_value". + - Empty a field of all text -> mode="clear_only". """ sb = _get_sb() - if mode == "type": + if mode == "fill_input": sb.type(selector, text, timeout=timeout) elif mode == "append": sb.send_keys(selector, text, timeout=timeout) - elif mode == "set_value": - sb.set_value(selector, text, timeout=timeout) elif mode == "fast_type": sb.fast_type(selector, text, timeout=timeout) - elif mode == "clear": + elif mode == "set_value": + sb.set_value(selector, text, timeout=timeout) + elif mode == "clear_only": sb.clear_input(selector, timeout=timeout) else: return ( f"Error: unknown mode '{mode}'. " - "Use 'type', 'append', 'set_value', 'fast_type', or 'clear'." + "Use 'fill_input', 'append', 'fast_type', " + "'set_value', or 'clear_only'." ) - return f"fill_input(mode={mode!r}) done for {selector}" + return f"type_text(mode={mode!r}) done for {selector}" @mcp.tool() @@ -915,7 +928,7 @@ def focus_on( - 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. + - Type text into a text field -> use type_text. - Hover -> use hover_with_action. """ sb = _get_sb() @@ -957,7 +970,7 @@ def wait_for( Use this tool when the page is dynamic and an automation step must wait for a condition before continuing. - Unlike check_state, this tool intentionally waits. Unlike assert_that, + Unlike check_state, this tool intentionally waits. Unlike assert_condition, its purpose is synchronization rather than validating a test expectation. Args: @@ -979,7 +992,7 @@ def wait_for( Tool selection: - Check current state immediately -> use check_state. - Wait for a state/content transition -> use wait_for. - - Verify an expected value/condition -> use assert_that. + - Verify an expected value/condition -> use assert_condition. """ sb = _get_sb() @@ -1009,11 +1022,11 @@ def wait_for( @mcp.tool() @handle_sb_errors -def assert_that( +def assert_condition( check: Literal[ "element_present", "element_visible", - "text", + "text_visible", "title", "url", "url_contains", @@ -1026,23 +1039,24 @@ def assert_that( """Verify an expected browser condition and fail when it is not met. Use this tool for explicit verification. Unlike check_state, which simply - reports the current state, assert_that treats a failed expectation as an - error. Unlike wait_for, URL/title checks do not wait. + reports the current state, assert_condition treats a failed expectation + as an error. Unlike wait_for, URL/title checks do not wait. Args: check: - "element_present": Verify selector identifies a present element. - "element_visible": Verify selector identifies a visible element. - - "text": Verify expected text within selector, or within the - whole HTML document when selector is omitted. + - "text_visible": Verify expected text is visible within selector, + or within the whole HTML document when selector is omitted. - "title": Verify the exact page title. - "url": Verify the exact current URL. - "url_contains": Verify that the current URL contains expected. selector: Element selector for element_present, element_visible, and - text checks. - expected: Expected text/title/URL value for text, title, url, and - url_contains. - exact: For check="text", require exact text rather than a substring. + text_visible checks. + expected: Expected text/title/URL value for text_visible, title, url, + and url_contains. + exact: For check="text_visible", require exact text + rather than a substring. timeout: Maximum seconds to wait for element/text checks. Returns: @@ -1055,14 +1069,16 @@ def assert_that( Tool selection: - Just inspect current state -> use check_state. - Wait for a condition to become true -> use wait_for. - - Verify that an expected condition is true -> use assert_that. + - Verify that an expected condition is true -> use assert_condition. """ sb = _get_sb() if check in ("element_present", "element_visible") and selector is None: return f"Error: check='{check}' requires value for `selector`." - if check in ("text", "title", "url", "url_contains") and expected is None: + if check in ( + "text_visible", "title", "url", "url_contains" + ) and expected is None: return f"Error: check='{check}' requires value for `expected`." if check == "element_present": @@ -1073,13 +1089,13 @@ def assert_that( sb.assert_element_visible(selector, timeout=timeout) return f"Confirmed {selector} is visible." - if check == "text": + if check == "text_visible": target = selector or "html" if exact: sb.assert_exact_text(expected, target, timeout=timeout) else: sb.assert_text(expected, target, timeout=timeout) - return f"Confirmed text in {target}." + return f"Confirmed visible text {expected} in {target}." if check == "title": sb.assert_title(expected) @@ -1095,7 +1111,8 @@ def assert_that( return ( f"Error: unknown check '{check}'. Use 'element_present', " - f"'element_visible', 'text', 'title', 'url', or 'url_contains'." + "'element_visible', 'text_visible', 'title', 'url', " + "or 'url_contains'." ) From 7fcaf6fc8c845413a19ceb6b087ba4353f4507d3 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 2 Sep 2026 01:28:00 -0400 Subject: [PATCH 4/5] Refresh Python dependencies --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b3a50c95a6e..dffc2298715 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.8" + "uv>=0.12.9" ], }, packages=[ From 3559308f489faeec20c4e241926580bdf8fc1bf8 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 2 Sep 2026 01:28:14 -0400 Subject: [PATCH 5/5] Version 4.53.5 --- seleniumbase/__version__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py index 70e4fe4a4fb..fdfdda49836 100755 --- a/seleniumbase/__version__.py +++ b/seleniumbase/__version__.py @@ -1,2 +1,2 @@ # seleniumbase package -__version__ = "4.53.4" +__version__ = "4.53.5"