From 3313dc2f0fee934c3c290000811a2a147c0afb35 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 13 Sep 2026 12:02:23 -0400 Subject: [PATCH 1/5] Update the MCP Servers --- cdp_server.py | 145 +++++++++++++++++++++++++++-------------------- driver_server.py | 2 +- sb_server.py | 2 +- 3 files changed, 85 insertions(+), 64 deletions(-) diff --git a/cdp_server.py b/cdp_server.py index b1daf8d..cb2c506 100644 --- a/cdp_server.py +++ b/cdp_server.py @@ -34,14 +34,15 @@ Tool-selection philosophy: - Use 'start_browser'/'close_browser' for opening/quitting the web browser. -- Use 'goto_url'/'manage_history' for browser navigation and history +- Use 'open_url'/'manage_history' for browser navigation and history inspection. - Use 'get_page_info' for reading browser/page metadata such as URL/title. - Use 'get_content'/'get_attributes' for reading text, HTML, or attributes. - Use 'find_elements' for discovering and inspecting multiple matching elements as structured data. -- Use 'check_condition' for an immediate, non-waiting state check. -- Use 'wait_for' when the agent needs to wait for a condition to become true. +- Use 'check_if_condition' for an immediate, non-waiting state check. +- Use 'wait_for_condition' when the agent needs to wait for a condition + to become true. - Use 'assert_condition' when the agent needs to verify an expected condition and treat failure as an assertion error. - Use 'click_element'/'type_text'/'select_option' for standard page @@ -111,7 +112,7 @@ def start_browser( ) -> str: """Launch a persistent SeleniumBase Pure CDP Mode browser session. - Call this before using browser interaction tools such as goto_url, + Call this before using browser interaction tools such as open_url, get_content, click_element, 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. @@ -303,8 +304,8 @@ def get_page_info() -> dict[str, Any]: - Need URL, title, origin, or User-Agent -> use get_page_info. - Need visible page text or HTML -> use get_content. - Need information about matching elements -> use find_elements. - - Need an immediate state check -> use check_condition. - - Need to wait for a condition -> use wait_for. + - Need an immediate state check -> use check_if_condition. + - Need to wait for a condition -> use wait_for_condition. - Need to verify an expected condition -> use assert_condition. Unlike a dedicated browser-status tool, get_page_info is the single @@ -338,8 +339,8 @@ def get_page_info() -> dict[str, Any]: @mcp.tool() @handle_sb_errors -def goto_url(url: str) -> str: - """Navigate the current browser tab to a URL. +def open_url(url: str) -> str: + """Navigate the current browser tab to the URL provided. Use this when the browser needs to visit a new URL rather than move through its existing back/forward history. @@ -350,17 +351,18 @@ def goto_url(url: str) -> str: Navigation waits for the browser's navigation operation to complete before returning. Dynamic content may still be loading; - use wait_for when synchronization is required. + use wait_for_condition when synchronization is required. + If there's an error, that gets propagated through @handle_sb_errors. Args: - url: Destination URL. May be a complete URL such as - "https://example.com" or a hostname such as "example.com". + url: The destination URL. May be a complete URL such as + "https://example.com", or a hostname such as "example.com". Returns: - A confirmation message containing the requested URL. + A confirmation message containing the requested URL if successful. Tool selection: - - Go to a new URL -> use goto_url. + - Navigate to a new URL -> use open_url. - Return to the previous page -> use manage_history(action="back"). - Go forward in history -> use manage_history(action="forward"). - Refresh the current page -> use manage_history(action="reload"). @@ -379,7 +381,7 @@ def manage_history( Use 'back' or 'forward' for history navigation, 'reload' to refresh while bypassing the cache, or 'list' to inspect history. - Use 'goto_url' for navigation to an arbitrary URL. + Use 'open_url' for navigation to an arbitrary URL. Args: action: @@ -477,7 +479,7 @@ def find_elements( use get_content. - Need to click one of several matches -> use click_element with nth. - Need to know whether an element is present/visible -> - use check_condition. + use check_if_condition. Notes: Element handles cannot be persisted across MCP calls. If you find @@ -542,8 +544,8 @@ def get_content( - Need visible text, html, or URLs on a page -> use get_content. - Need structured information about matching elements -> use find_elements. - - Need to check element presence/visibility -> use check_condition. - - Need to wait for content to appear -> use wait_for. + - Need to check element presence/visibility -> use check_if_condition. + - Need to wait for content to appear -> use wait_for_condition. If there's no matching element found within the timeout, then @handle_sb_errors returns details from the exception raised. @@ -592,7 +594,8 @@ def get_attributes( - 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 element presence/visibility -> use 'check_condition'. + - Need to check element presence/visibility -> + use 'check_if_condition'. This is a read-only operation. @@ -609,7 +612,7 @@ def get_attributes( @mcp.tool() @handle_sb_errors -def check_condition( +def check_if_condition( check: Literal["present", "visible"] = "visible", selector: str = "body", text: str | None = None, @@ -618,9 +621,10 @@ def check_condition( for the condition to become true. Use this tool when you need an immediate boolean observation of the current - page state. Use wait_for when the condition may become true later and the - workflow should wait for it. Use assert_condition when the condition is an - expected requirement and failure should be treated as an assertion error. + page state. Use wait_for_condition when the condition may become true later + and the workflow should wait for it. Use assert_condition when the + condition is an expected requirement and failure should be treated as an + assertion error. Args: check: @@ -645,8 +649,8 @@ def check_condition( exception. If there's an error, returns a string with error details. Tool selection: - - Immediate boolean observation -> use check_condition. - - Wait for a state/content transition -> use wait_for. + - Immediate boolean observation -> use check_if_condition. + - Wait for a state/content transition -> use wait_for_condition. - Verify an expected condition -> use assert_condition. - Need element details of matching elements -> use find_elements. - Need to read page or element content -> use get_content. @@ -654,7 +658,7 @@ def check_condition( Notes: 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 instead. + asynchronous loading matters, use wait_for_condition instead. When `text` is provided, `check` is ignored. """ @@ -709,7 +713,7 @@ def click_element( clicks; do not use them with `all_matches=True`. nth: 1-based occurrence to click when multiple elements match. - Must be >= 1. Takes precedence over `all_matches`, + Must be >= 1 if provided. Takes precedence over `all_matches`, `only_if_visible`, and `parent_selector`. all_matches: If True, click every currently visible matching element @@ -744,7 +748,7 @@ def click_element( """ sb = _get_sb() - if nth is not None: + if nth: if nth < 1: return "Error: nth must be >= 1." sb.click_nth_element(selector, nth, scroll=scroll) @@ -769,9 +773,10 @@ def click_element( @mcp.tool() @handle_sb_errors def hover_action( - selector1: str, - selector2: str | None = None, + selector: str, + secondary_selector: str | None = None, action: Literal["hover", "hover_and_click", "drag_and_drop"] = "hover", + timeout: float = 5, ) -> str: """Hover over an element, optionally click another, or drag-and-drop. @@ -779,51 +784,67 @@ def hover_action( drag-and-drop operations. Args: - selector1: + selector: The primary element selector. For action="hover", this is the element to hover over. For action="hover_and_click", this is the element to hover over - before clicking selector2. + before clicking 'secondary_selector'. For action="drag_and_drop", this is the draggable source element. - selector2: + secondary_selector: The secondary element selector. Required for action="hover_and_click", where it identifies - the element revealed or targeted after hovering selector1. + the element to click after hovering 'selector'. Required for action="drag_and_drop", where it identifies the destination/drop target. Not used for action="hover". action: - - "hover": Hover over selector1 only. - - "hover_and_click": Hover over selector1, then click selector2. - - "drag_and_drop": Drag selector1 and drop it onto selector2. + - "hover": Hover over 'selector' only. + - "hover_and_click": Hover over 'selector', then click + 'secondary_selector' after a short moment has passed. + - "drag_and_drop": Drag 'selector' and drop it onto + 'secondary_selector'. + + timeout: Maximum seconds to wait for 'selector'. + For drag_and_drop, the same timeout applies to secondary_selector. + For hover_and_click, SeleniumBase uses its own short wait for + secondary_selector; this parameter does not extend that secondary + wait. Returns: - A confirmation message describing the performed operation. + A confirmation message describing the performed operation's result. + + Errors: + If a required element cannot be found or interacted with within the + applicable wait period, or if an error occurs during the action, the + resulting exception message is returned through @handle_sb_errors. - Tool selection: - - Simple hover -> action="hover". - - Hover over one element and click another -> action="hover_and_click". - - Drag one element onto another -> action="drag_and_drop". """ sb = _get_sb() + if timeout < 0: + return "Error: timeout must be >= 0." + if action == "hover": - sb.hover_element(selector1) - return f"Hovered {selector1}" + sb.hover_element(selector, timeout=timeout) + return f"Hovered {selector}" if action == "hover_and_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 not secondary_selector: + return ( + "Error: action='hover_and_click' requires secondary_selector." + ) + sb.hover_and_click(selector, secondary_selector, timeout=timeout) + return f"Hovered {selector} and clicked {secondary_selector}" 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}" + if not secondary_selector: + return ( + "Error: action='drag_and_drop' requires secondary_selector." + ) + sb.drag_and_drop(selector, secondary_selector, timeout=timeout) + return f"Dragged {selector} onto {secondary_selector}" return ( f"Error: unknown action '{action}'. " @@ -998,7 +1019,7 @@ def focus_element( @mcp.tool() @handle_sb_errors -def wait_for( +def wait_for_condition( state: Literal[ "present", "visible", @@ -1017,7 +1038,7 @@ def wait_for( condition is met or the timeout expires. It does not intentionally scroll, click, or otherwise modify the page while waiting. - Use check_condition to inspect the current state without waiting. + Use check_if_condition to inspect the current state without waiting. Use assert_condition to verify an expected condition rather than synchronize with a changing page. @@ -1056,8 +1077,8 @@ def wait_for( the tool returns the error produced by its error handler. Tool selection: - - Inspect current state immediately -> check_condition. - - Wait for a state change -> wait_for. + - Inspect current state immediately -> check_if_condition. + - Wait for a state change -> wait_for_condition. - Verify an expectation -> assert_condition. """ sb = _get_sb() @@ -1132,10 +1153,10 @@ def assert_condition( handled by `handle_sb_errors` and returned as a descriptive tool error; it is not reported as a successful result. - Unlike check_condition, this tool does not merely return whether a + Unlike check_if_condition, this tool does not merely return whether a condition is true: a failed expectation is an error. - Unlike wait_for, its purpose is to verify an expectation, not merely - synchronize with a changing page. + Unlike wait_for_condition, its purpose is to verify an expectation, + not merely synchronize with a changing page. Args: check: @@ -1169,8 +1190,8 @@ def assert_condition( instead of a success message. Tool selection: - - Inspect a condition without failing -> check_condition. - - Wait for a condition to become true -> wait_for. + - Inspect a condition without failing -> check_if_condition. + - Wait for a condition to become true -> wait_for_condition. - Verify that an expected condition is true -> assert_condition. """ sb = _get_sb() @@ -1499,7 +1520,7 @@ def manage_tabs( """Manage browser tabs, including opening new ones. Use this for listing, opening, switching, or closing tabs. - Use `goto_url` and `manage_history` for navigation within the active tab. + Use `open_url` and `manage_history` for navigation within the active tab. Args: action: @@ -1596,7 +1617,7 @@ def solve_captcha() -> str: 1. Inspect the webpage with get_content when you need to determine whether CAPTCHA-related controls are present. 2. Call solve_captcha to attempt the CAPTCHA interaction. - 3. Use get_page_info, get_content, check_condition, + 3. Use get_page_info, get_content, check_if_condition, or manage_cookies to inspect resulting page/session state. Returns: diff --git a/driver_server.py b/driver_server.py index d4a28ce..df757d7 100644 --- a/driver_server.py +++ b/driver_server.py @@ -139,7 +139,7 @@ def close_browser() -> str: @mcp.tool() @handle_sb_errors -def goto_url(url: str) -> str: +def open_url(url: str) -> str: """Navigate to the given URL in the web browser. If the URL doesn't start with a protocol (eg: `https://`), then `https://` is automatically prefixed in before navigation. diff --git a/sb_server.py b/sb_server.py index 0fe1334..32468bd 100644 --- a/sb_server.py +++ b/sb_server.py @@ -193,7 +193,7 @@ def page_snapshot() -> dict: @mcp.tool() @handle_sb_errors -def goto_url(url: str) -> str: +def open_url(url: str) -> str: """Navigate to the given URL in the web browser. If the URL doesn't start with a protocol (eg: `https://`), then `https://` is automatically prefixed in before navigation. From 7ca7b74daa6dc6475e018d76f1c8410705c2d52d Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 13 Sep 2026 12:02:35 -0400 Subject: [PATCH 2/5] Update the ReadMe --- README.md | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 4cc88a2..8eb447c 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ The location of `claude_desktop_config.json` depends on your system: - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` - Windows: `%APPDATA%\Claude\claude_desktop_config.json` -Restart Claude Desktop. You should see a 🔨 tools icon indicating the server(s) connected, with tools like `start_browser`, `goto_url`, `click_element`, etc. available. Only keep the entries you actually want. Three separate browser-automation servers is a lot if you only need one. +Restart Claude Desktop. You should see a 🔨 tools icon indicating the server(s) connected, with tools like `start_browser`, `open_url`, `click_element`, etc. available. Only keep the entries you actually want. Three separate browser-automation servers is a lot if you only need one. ## 4. Connect it to Claude Code @@ -145,7 +145,7 @@ claude mcp add seleniumbase-sb -- uv run seleniumbase-sb |---|---| | `start_browser(browser, headless, uc, incognito)` | Launch a browser session (headless defaults to `False`) | | `close_browser()` | End the session | -| `goto_url(url)` | Go to a URL | +| `open_url(url)` | Go to a URL | | `go_back()` / `go_forward()` / `refresh_page()` | History navigation | | `get_current_url()` / `get_title()` | Page metadata | | `get_page_source()` | Full HTML | @@ -199,10 +199,10 @@ in the loop at all. Reference: | Group | Tool(s) | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | Session | `start_browser(url, headless, use_chromium, browser_executable_path, incognito, guest, ad_block, proxy)`, `close_browser` | -| Navigation | `goto_url`, `manage_history(action: back/forward/reload/list)`, `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)`, `get_attributes`, `check_state(check: present/visible/count/text_visible)` | +| Navigation | `open_url`, `manage_history(action: back/forward/reload/list)`, `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)`, `get_attributes`, `check_for_condition(check: present/visible/count/text_visible)` | | Interacting | `click_element(selector, nth, all_matches, only_if_visible, parent_selector, timeout, scroll)`, `hover_action(selector1, selector2, action: hover/hover_and_click/drag_and_drop)`, `type_text(mode: fill_input/append/fast_type/set_value/clear_only)`, `select_option(by: text/value/index)`, `focus_element(action: scroll_to_element/focus/highlight)` | -| Waiting | `wait_for(state: present/visible/not_visible/absent/seconds_passed, text)` | +| Waiting | `wait_for_condition(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_page(direction: up/down/top/bottom, amount)` | @@ -226,12 +226,6 @@ in the loop at all. Reference: - **Session teardown.** `sb.quit()` (used by `close_browser`) is the documented way to end a session; the browser also auto-closes if the process exits without it. -- **Not wrapped:** PyAutoGUI-based `gui_*` methods (excluded by design — - see the top-level design notes), low-level plumbing - (`get_websocket_url`, `add_handler`, permission grants, raw - `get_document`/`get_flattened_document`), and exact method aliases - (`open`/`goto` vs `get`) were left out to keep the tool list focused — - add them the same way as any other tool if you need them. --- From c8245566d47c83050409a6e741747d6181a2333c Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 13 Sep 2026 12:02:59 -0400 Subject: [PATCH 3/5] Refresh Python dependencies --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 4493a04..00ef4aa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -seleniumbase[mcp]>=4.54.3 +seleniumbase[mcp]>=4.54.4 mcp[cli]>=2.2.0,<3.0.0 diff --git a/setup.py b/setup.py index 3da46f1..6725ec6 100755 --- a/setup.py +++ b/setup.py @@ -140,7 +140,7 @@ ], python_requires=">=3.10", install_requires=[ - "seleniumbase[mcp]>=4.54.3", + "seleniumbase[mcp]>=4.54.4", "mcp[cli]>=2.2.0,<3.0.0", ], extras_require={ From 056c6009a8a979a01735e51a61750f2c1267077a Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 13 Sep 2026 12:03:18 -0400 Subject: [PATCH 4/5] Update the unit tests --- tests/test_mcp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_mcp.py b/tests/test_mcp.py index c76b382..f2547ee 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -24,7 +24,7 @@ async def test_server(name: str, command: str) -> None: assert "start_browser" in tools assert "close_browser" in tools - assert "goto_url" in tools + assert "open_url" in tools result = await client.call_tool( "start_browser", @@ -33,7 +33,7 @@ async def test_server(name: str, command: str) -> None: assert not result.is_error result = await client.call_tool( - "goto_url", + "open_url", { "url": ( "data:text/html," From 858a70915e1e8ba9f34cf96a827d83e3a85489e5 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Sun, 13 Sep 2026 12:03:34 -0400 Subject: [PATCH 5/5] Version 1.3.4 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6725ec6..161cba0 100755 --- a/setup.py +++ b/setup.py @@ -70,7 +70,7 @@ setup( name="seleniumbase-mcp", - version="1.3.3", + version="1.3.4", description="MCP servers exposing SeleniumBase as tools for MCP clients.", long_description=long_description, long_description_content_type="text/markdown",