diff --git a/help_docs/cdp_mode_methods.md b/help_docs/cdp_mode_methods.md index 1e28e3edbf7..2b1b2e51ca8 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) +sb.get_all_urls(absolute=True, selector=None) sb.get_all_cookies(*args, **kwargs) sb.set_all_cookies(*args, **kwargs) sb.save_cookies(*args, **kwargs) diff --git a/mcp_servers/README.md b/mcp_servers/README.md index 075b3deaf61..7426c7096ad 100644 --- a/mcp_servers/README.md +++ b/mcp_servers/README.md @@ -8,7 +8,7 @@ This server, (located in `server.py`), uses SeleniumBase's [Pure CDP Mode](https Other SeleniumBase automation styles, (such as `Driver()` and `SB()`), have their own MCP servers in [seleniumbase/seleniumbase-mcp](https://github.com/seleniumbase/seleniumbase-mcp). -Defaults to `headless=False`. Use `headless=True` when starting a session for headless mode, which may be less stealthy. +`headless` defaults to `None` in `start_browser`, which resolves to headless on Linux (typical for server/container environments) and headed on Windows/macOS. Pass `headless=True` or `headless=False` explicitly to override this for any OS; headless mode may be less stealthy. ## 1. Install @@ -91,21 +91,19 @@ Restart Claude Desktop. You should see a 🔨 tools icon indicating the server c * `start_browser` * `close_browser` -* `browser_status` * `navigate` * `navigate_history` * `get_page_info` * `find_elements` -* `get_page_content` +* `get_content` * `get_attributes` * `check_state` -* `get_all_urls` * `click` * `hover` * `drag_and_drop` * `fill_input` * `select_option` -* `element_action` +* `act_on_element` * `wait_for` * `assert_that` * `manage_cookies` @@ -117,7 +115,6 @@ Restart Claude Desktop. You should see a 🔨 tools icon indicating the server c * `save_output` * `run_javascript` * `wait_seconds` -* `get_user_agent` ## 4. Connect it to Claude Code @@ -157,17 +154,17 @@ Tools here are grouped around a shared `selector` convention: `selector` args ac | Group | Tool(s) | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| Session | `start_browser(url, headless, use_chromium, browser_executable_path, incognito, guest, ad_block, proxy)`, `close_browser`, `browser_status` | -| Navigation | `navigate`, `navigate_history(action: back/forward/reload)`, `get_page_info` (url, title, origin, history in one call) | -| Finding & reading | `find_elements(selector, timeout, include_html)`, `get_page_content(selector, as_html, include_shadow_dom)`, `get_attributes`, `check_state(check: present/visible/count/text_visible)`, `get_all_urls` | -| Interacting | `click(selector, nth, all_matches, only_if_visible, parent_selector, timeout, scroll)`, `hover(selector, then_click_selector)`, `drag_and_drop`, `fill_input(mode: type/append/set_value/fast_type/clear)`, `select_option(by: text/value/index)`, `element_action(action: focus/highlight/scroll_into_view)` | +| Session | `start_browser(url, headless, use_chromium, browser_executable_path, incognito, guest, ad_block, proxy)`, `close_browser` | +| Navigation | `navigate`, `navigate_history(action: back/forward/reload)`, `get_page_info` (running status, url, title, origin, user agent, history in one call) | +| Finding & reading | `find_elements(selector, timeout, include_html)`, `get_content(selector, output_format: text/html/urls, include_shadow_dom)`, `get_attributes`, `check_state(check: present/visible/count/text_visible)` | +| Interacting | `click(selector, nth, all_matches, only_if_visible, parent_selector, timeout, scroll)`, `hover(selector, then_click_selector)`, `drag_and_drop`, `fill_input(mode: type/append/set_value/fast_type/clear)`, `select_option(by: text/value/index)`, `act_on_element(action: focus/highlight/scroll_into_view)` | | Waiting | `wait_for(state: present/visible/not_visible/absent, text)` | | Assertions | `assert_that(check: element_present/element_visible/text/title/url/url_contains)` | | Cookies & storage | `manage_cookies(action: get_all/clear/save/load)`, `manage_storage(storage: local/session, action: get/set)` | | 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`, `wait_seconds`, `get_user_agent` | +| Output & misc | `save_output(format: screenshot/html/pdf)`, `run_javascript`, `wait_seconds` | ## Design notes / things to adapt for your use case diff --git a/mcp_servers/pyproject.toml b/mcp_servers/pyproject.toml index 662e15db899..87870e7176c 100644 --- a/mcp_servers/pyproject.toml +++ b/mcp_servers/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "seleniumbase-mcp" -version = "1.2.1dev0" +version = "1.2.1dev1" 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 4530f5094b2..2e3fedc6147 100644 --- a/mcp_servers/server.py +++ b/mcp_servers/server.py @@ -13,15 +13,26 @@ Model: One persistent `sb_cdp.Chrome` session per server process. Call start_browser once; drive it with the other tools; then close_browser. -Design notes (v2): -Tools are grouped around one CSS-selector-or-text-matched-by convention: -`selector` args accept a CSS selector, or visible text (e.g. -'a:contains("Sign in")'). Where the original tool set had several -near-identical tools for one concept (e.g. five click variants, five wait -variants, eight cookie/storage variants), those are now a single tool with -a mode/action/state/check parameter, to reduce the number of near-neighbor -tools an agent has to disambiguate between while keeping every underlying -capability available. +Design notes: +Tools follow a consistent CSS-selector-or-text matching convention: +selector arguments accept a CSS selector or visible text (for example, +a:contains("Sign in")). Related SeleniumBase capabilities are consolidated +into parameterized tools using action, mode, state, or check parameters. +This keeps the toolset compact and predictable while giving an MCP client +access to the underlying browser-automation capabilities without +having to choose between multiple near-identical tools. + +Tool-selection philosophy: +- Use get_page_info for browser/page metadata such as URL, title, origin, + and navigation history. +- Use get_content for reading visible text or HTML. +- Use find_elements for discovering and inspecting multiple matching + 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/act_on_element for interactions. Note on elements: CDP-mode element objects (from find_element/find_all) are live handles with their own methods (.click(), .get_html(), ...) that can't @@ -44,14 +55,20 @@ def _get_sb() -> sb_cdp.CDPMethods: + """Return the active browser session or raise a useful lifecycle error.""" if _sb is None: raise RuntimeError("No browser session. Call start_browser first.") return _sb def handle_sb_errors(func): - """Catches SeleniumBase errors and surfaces them as descriptive strings - so the LLM agent can read them and self-correct.""" + """Convert SeleniumBase/runtime exceptions into descriptive MCP results. + + Browser automation failures are returned as readable error strings so + an MCP client/LLM can inspect the error and decide whether to retry, + change a selector, wait for a condition, navigate elsewhere, or take + another corrective action. + """ @wraps(func) def wrapper(*args, **kwargs): try: @@ -60,6 +77,7 @@ def wrapper(*args, **kwargs): error_type = e.__class__.__name__ error_msg = str(e).strip() return f"Error in {func.__name__}: {error_type} - {error_msg}" + return wrapper @@ -70,7 +88,7 @@ def wrapper(*args, **kwargs): @mcp.tool() def start_browser( url: str | None = None, - headless: bool = False, + headless: bool | None = None, use_chromium: bool = False, browser_executable_path: str | None = None, incognito: bool = False, @@ -78,37 +96,101 @@ def start_browser( ad_block: bool = False, proxy: str | None = None, ) -> str: - """Launch a Pure CDP Mode browser session. Must be called before any - other tool. The browser is driven entirely over CDP (no WebDriver), - which is SeleniumBase's most stealth/bot-detection-resistant mode. + """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 + session remains active across subsequent MCP tool calls until + close_browser is called or the server process exits. + + Pure CDP Mode communicates directly with the browser through the Chrome + DevTools Protocol rather than WebDriver. This provides SeleniumBase's + CDP-based browser automation capabilities without using WebDriver as the + browser-control layer. + Args: - url: Optional URL to open immediately on launch. - headless: Run without a visible browser. (Mainly for macOS or Windows - because Xvfb automatically provides a virtual display on Linux.) + url: Optional URL to open immediately after the browser launches. + If omitted, the browser starts without navigating to a requested + page. + + headless: Controls whether the browser runs without a visible window. + If True, always run headless. If False, always run headed. + If omitted (None), the default depends on the operating system: + Linux defaults to headless because MCP/server environments + commonly do not have a graphical desktop, while Windows and macOS + default to headed so that a visible browser window is available. + Use True or False to explicitly override the OS-specific default + on any operating system. + use_chromium: Use Chromium instead of Google Chrome. This is useful - on environments where Google Chrome is not installed because - SeleniumBase automatically downloads Chromium if it's not found. - browser_executable_path: If Google Chrome is not installed in the - default location, you can set the direct path with this arg. - (This option should not be used if setting use_chromium to True.) - incognito: Launch in a private/incognito window. - guest: Launch in Chrome guest mode. (Don't use with incognito mode) - ad_block: Enables basic ad-blocking functionality. - proxy: Proxy string, e.g. "USER:PASS@SERVER:PORT" or "SERVER:PORT". + when Google Chrome is not installed. SeleniumBase can manage the + Chromium browser when this option is enabled. + + browser_executable_path: Explicit filesystem path to the browser + executable when it is not installed in a standard location. + Do not combine this with use_chromium=True. + + incognito: Launch Chrome/Chromium in incognito mode. + + guest: Launch Chrome/Chromium in guest mode. Do not combine this with + incognito=True. + + ad_block: Enable SeleniumBase's basic ad-blocking functionality. + + proxy: Optional proxy server. Examples include + "SERVER:PORT" or "USER:PASS@SERVER:PORT". + + Returns: + A confirmation message when the browser starts successfully, including + the effective headless setting, or a descriptive error when browser + startup fails. + + Lifecycle: + Call start_browser once at the beginning of a browser automation + workflow. Reusing the existing session preserves cookies, tabs, + navigation history, localStorage/sessionStorage, and other browser + state between tool calls. Call close_browser when finished. + + Environment requirements: + The MCP runtime must have a compatible Chrome or Chromium browser + available. If the browser executable cannot be discovered, use + use_chromium=True or provide browser_executable_path explicitly. + + On Linux, the default is headless=True so the browser can run in + typical server/container environments without a graphical desktop. + Set headless=False when a graphical display is available and a visible + browser is desired. On Windows and macOS, the default is + headless=False. Set headless=True when running without a desktop or + when a visible browser window is not desired. """ global _sb + if _sb is not None: return ( - "A browser session is already running. Call close_browser first." + "A browser session is already running. " + "Call close_browser first." ) + if incognito and guest: return "Error: incognito and guest cannot both be enabled." + if use_chromium and browser_executable_path: return ( "Error: use_chromium and browser_executable_path " "cannot both be used at the same time." ) - kwargs: dict[str, Any] = {"headless": headless} + + # OS-specific default: + # - Linux: headless by default for server/container compatibility. + # - Windows/macOS: headed by default for interactive desktop use. + # - Explicit True/False always overrides the OS default. + if headless is None: + effective_headless = sys.platform.startswith("linux") + else: + effective_headless = headless + + kwargs: dict[str, Any] = {"headless": effective_headless} + if use_chromium: kwargs["use_chromium"] = True if browser_executable_path: @@ -121,11 +203,12 @@ def start_browser( kwargs["ad_block"] = True if proxy: kwargs["proxy"] = proxy + try: _sb = sb_cdp.Chrome(url, **kwargs) return ( f"Started Pure CDP Mode browser " - f"(url={url!r}, headless={headless}, " + f"(url={url!r}, headless={effective_headless}, " f"use_chromium={use_chromium})" ) except Exception as e: @@ -135,6 +218,7 @@ def start_browser( except Exception: pass _sb = None + return ( f"Error starting browser: " f"{e.__class__.__name__} - {str(e).strip()}" @@ -143,31 +227,71 @@ def start_browser( @mcp.tool() def close_browser() -> str: - """Close the browser and end the session. - This tool should be called after you have finished using the browser - so that you can conclude the session and free up any resources that - were being used by Chrome/Chromium. If you need to perform more - browser actions after using this tool, then you'll need to use the - 'start_browser' tool again in order to begin a new browser session.""" + """Close the active browser session and release browser resources. + + Call this when the browser automation workflow is finished. Closing the + session ends the persistent browser state, including its open tabs, + cookies, navigation history, and page state. If browser automation is + needed afterward, start a new session with start_browser. + + This operation is safe to call when no browser session is active. + """ global _sb + if _sb is None: return "No browser session was running." + try: _sb.quit() except Exception: pass + _sb = None return "Browser closed." +# --------------------------------------------------------------------------- +# Page information +# --------------------------------------------------------------------------- + @mcp.tool() -def browser_status() -> dict: - """Return whether a browser session is currently active, along with - the URL and title of the current page if the browser session is active. - This tool can be used for debugging, as well as for gathering basic - info on the current page. For instance, if a click action navigates - the browser window to a new page, you can use this tool to find out - the URL/title of the new page that the browser window navigated to.""" +@handle_sb_errors +def get_page_info() -> dict | str: + """Get current browser session and page metadata. + + Use this as the primary tool for determining where the browser currently + is after navigation, clicks, form submissions, redirects, reloads, or + tab switches. + + 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. + + Returns: + A dictionary containing: + - running: True when a browser session is active. + - url: The complete current page URL, including path and query string. + - title: The current document title. + - origin: The current page origin (scheme, host, and port). + - user_agent: The browser's current User-Agent string. + - history: The browser navigation history for the current session. + + Tool selection: + - Need URL, title, origin, User-Agent, or navigation history -> + 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_state. + - Need to wait for a condition -> use wait_for. + - Need to verify an expected condition -> use assert_that. + + Unlike a dedicated browser-status tool, get_page_info is the single + source of browser/page metadata. If no browser session is active, it + returns {"running": False} instead of attempting to access a page. + + This operation does not navigate, reload, click, type, or otherwise + modify the current page. + """ if _sb is None: return {"running": False} @@ -176,6 +300,9 @@ def browser_status() -> dict: "running": True, "url": _sb.get_current_url(), "title": _sb.get_title(), + "origin": _sb.get_origin(), + "user_agent": _sb.get_user_agent(), + "history": _sb.get_navigation_history(), } except Exception as e: return { @@ -191,14 +318,32 @@ def browser_status() -> dict: @mcp.tool() @handle_sb_errors def navigate(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. - (eg: "seleniumbase.io" becomes "https://seleniumbase.io") - Waits until the initial HTML document is fully parsed and loaded. - New pages visited will show up in browser navigation history. - If the URL is invalid or the page can't load due to an issue, - then the corresponding errors will be raised.""" + """Navigate the current browser tab to a URL. + + Use this when the browser needs to visit a new URL rather than move + through its existing back/forward history. + + If the URL does not include a protocol such as "https://", SeleniumBase + automatically prefixes "https://" before navigation. For example, + "seleniumbase.io" becomes "https://seleniumbase.io". + + Navigation waits for the initial HTML document to be loaded before + returning. The visited page becomes part of the browser's navigation + history. + + Args: + url: Destination URL. May be a complete URL such as + "https://example.com" or a hostname such as "example.com". + + Returns: + A confirmation containing the requested URL. + + Tool selection: + - Go to a new URL -> use navigate. + - Return to the previous page -> use navigate_history(action="back"). + - Go forward in history -> use navigate_history(action="forward"). + - Refresh the current page -> use navigate_history(action="reload"). + """ _get_sb().get(url) return f"Navigated to {url}" @@ -208,76 +353,53 @@ def navigate(url: str) -> str: def navigate_history( action: Literal["back", "forward", "reload"] = "back", ) -> str: - """Navigate through the current browser history or reload the current page. - Use this tool to move backward or forward through pages already visited in - the current browser session, or to reload the current page when its latest - content should be fetched again. This is useful for returning to a - previously visited page, retracing browser navigation, recovering from - accidental navigation, or refreshing page content. + """Navigate through the current browser history or reload the page. + + Use this tool only for navigation relative to the current browser + history. Use navigate when going to an arbitrary URL. + Args: - action: The browser navigation operation to perform: - - "back": Navigate to the previous page in the browser history. - Has no effect if there is no previous history entry. - - "forward": Navigate to the next page in the browser history. - Has no effect if there is no forward history entry. + action: + - "back": Navigate to the previous history entry. Has no useful + effect when there is no previous history entry. + - "forward": Navigate to the next history entry. Has no useful + effect when there is no forward history entry. - "reload": Reload the current page while ignoring the browser - cache, forcing the page resources to be fetched again. + cache so page resources are fetched again. + Returns: - A confirmation message describing the navigation operation that was - performed. + A confirmation message describing the operation performed. + Notes: - These operations modify the current browser page and may trigger page - loads, redirects, or other navigation events. After navigation, use - page-information or page-content tools when you need to verify the - resulting URL, title, or page state. - "back" and "forward" operate on the browser's existing navigation - history; they do not navigate to an arbitrary URL. "reload" stays on - the current page but bypasses the browser cache. + These operations can trigger page loads, redirects, and other + navigation events. Use get_page_info afterward when you need to + verify the resulting URL or title. + + Tool selection: + - Arbitrary destination URL -> use navigate. + - Previous/next browser history entry -> use this tool. + - Refresh current page -> use this tool with action="reload". """ sb = _get_sb() + if action == "back": sb.go_back() return "Navigated back." + if action == "forward": sb.go_forward() return "Navigated forward." + if action == "reload": sb.reload(ignore_cache=True) return "Page reloaded." + return ( f"Error: unknown action '{action}'. " "Use 'back', 'forward', or 'reload'." ) -@mcp.tool() -@handle_sb_errors -def get_page_info() -> dict | str: - """Get comprehensive information about the current browser page. - Returns the current URL, document title, origin, and navigation history. - Use this tool after navigation or page interactions when you need to verify - where the browser is currently located, identify the loaded page, determine - the site's origin, or inspect how the browser reached the current page. - Returns: - A dictionary containing: - - url: The current page URL, including the path and query string. - - title: The current document title. - - origin: The page origin (scheme, host, and port), useful for - identifying the current website or comparing cross-origin navigation. - - history: The browser's navigation history, useful for understanding - the sequence of pages visited during the current session. - This is a read-only operation and does not navigate, reload, or modify - the current page. - """ - sb = _get_sb() - return { - "url": sb.get_current_url(), - "title": sb.get_title(), - "origin": sb.get_origin(), - "history": sb.get_navigation_history(), - } - - # --------------------------------------------------------------------------- # Finding & reading # --------------------------------------------------------------------------- @@ -289,19 +411,47 @@ def find_elements( timeout: int | float | None = 7, include_html: bool = False, ) -> dict | str: - """Find element(s) matching a CSS selector or visible text, and return - their tag name, text, and outer HTML (optional). + """Find matching elements and return structured element information. + + Use this tool when you need to discover how many elements match a + selector, inspect their text/tag names, or inspect the HTML of multiple + matches. + + This tool resolves element handles immediately into ordinary JSON-like + dictionaries. It does not return live SeleniumBase element objects. + Args: - selector: CSS selector, or text to search for (CDP Mode can match - elements by selector or visible text, eg. 'a:contains("Sign in")'). - timeout: Seconds to wait for at least one match to appear. - include_html: Whether to include the html with each matching element. - Returns a dict with 'count' (total matches found) and 'matches' (a list - of {tag_name, text, html} dicts, or {tag_name, text} dicts if not - including html). + selector: CSS selector, or a SeleniumBase selector that can match + visible text. Examples include "button", ".login-link", or + 'a:contains("Sign in")'. + timeout: Maximum number of seconds to wait for matching elements. + Defaults to 7 seconds. + include_html: If True, include each matching element's outer HTML. + If False, return only tag name and text. + + Returns: + A dictionary containing: + - count: Number of matching elements found. + - matches: A list of element dictionaries containing tag_name and + text, plus html when include_html=True. + + Tool selection: + - Need structured information about matching elements -> + use find_elements. + - Need the visible text/HTML of a page or a single element -> + use get_content. + - Need to click one of several matches -> use click with nth. + - Need to know whether an element is present/visible -> + use check_state. + + Note: + 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. """ sb = _get_sb() els = sb.find_all(selector, timeout=timeout) + if include_html: return { "count": len(els), @@ -310,57 +460,118 @@ def find_elements( "tag_name": e.tag_name, "text": e.text, "html": e.get_html(), - } for e in els - ], - } - else: - return { - "count": len(els), - "matches": [ - { - "tag_name": e.tag_name, - "text": e.text, - } for e in els + } + for e in els ], } + return { + "count": len(els), + "matches": [ + { + "tag_name": e.tag_name, + "text": e.text, + } + for e in els + ], + } + @mcp.tool() @handle_sb_errors -def get_page_content( +def get_content( selector: str | None = None, - as_html: bool = False, + output_format: Literal["text", "html", "urls"] = "text", include_shadow_dom: bool = True, -) -> str: - """Get the visible text or HTML of an element, or of the whole page. - You can optionally include any shadow-root elements (default: True). +) -> str | list[str]: + """Read visible text, HTML, or discovered URLs from the current page. + + Use this tool when you need actual page content or URL information rather + than page metadata. + Args: - selector: Element to read from. Omit (or pass None) to read the - whole page instead of one element. - as_html: If True, return HTML instead of visible text. - include_shadow_dom: Only applies when reading the whole page as HTML. + 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. + + output_format: + - "text": Return visible text from the page or selected element. + - "html": Return HTML from the page or selected element. + - "urls": Return all discovered linked/resource URLs on the page + or within the selected element. URLs associated with elements + such as anchors, links, images, scripts, and metadata may be + included. SeleniumBase returns full URLs with their URL + 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. + + Tool selection: + - Need URL, title, origin, User-Agent, or navigation history -> + use get_page_info. + - Need visible text -> use output_format="text". + - Need page or element HTML -> use output_format="html". + - Need URLs from the page or an element -> use output_format="urls". + - Need structured information about matching elements -> + use find_elements. + - Need to check whether an element is present or visible -> + use check_state. + - Need to wait for content to appear -> use wait_for. """ sb = _get_sb() + + if output_format == "urls": + return sb.get_all_urls(selector=selector) + if selector is None: - if as_html: - return sb.get_page_source(include_shadow_dom=include_shadow_dom) + if output_format == "html": + return sb.get_page_source( + include_shadow_dom=include_shadow_dom + ) return sb.get_text("body") - if as_html: + + if output_format == "html": return sb.get_element_html(selector) + return sb.get_text(selector) @mcp.tool() @handle_sb_errors -def get_attributes(selector: str, attribute: str | None = None) -> Any: - """Get one attribute's value from an element, or all of its attributes - as a dict if `attribute` isn't given. +def get_attributes( + selector: str, + attribute: str | None = None, +) -> Any: + """Read HTML attributes from a matching element. + Args: - selector: The CSS selector of the element to get the attribute from. - attribute: The HTML attribute of the element that you want to get.""" + 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. + + Returns: + The requested attribute value, or a dictionary containing all + attributes when attribute is omitted. + + This is a read-only operation and does not modify the element. + """ sb = _get_sb() + if attribute: return sb.get_element_attribute(selector, attribute) + return sb.get_element_attributes(selector) @@ -371,48 +582,62 @@ def check_state( selector: str = "body", text: str | None = None, ) -> Any: - """Check the current state of the page or an element. - Unless setting 'count', where it may wait up to 1 second, this never waits. - Never raises exceptions — use wait_for if you want to wait for a state. - For 'count', if there are no matching elements, then it waits up to - 1 second for a single match to appear. If no matches after 1 second, - then 'count' returns 0. + """Immediately inspect the current state of an element or page. + + 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. + Args: - check: 'present', 'visible', 'count', 'text_visible'. - (`text_visible` requires value for `text`.) - selector: The CSS Selector for the chosen check. - text: The text to use for the `text_visible` check. + check: + - "present": Return whether at least one matching element exists. + - "visible": Return whether the matching element is visible. + - "count": Return the number of matching elements. This check may + wait up to 1 second for a match. + - "text_visible": Return whether the specified text is visible + within the selected element. Requires text. + selector: CSS selector or SeleniumBase selector for the element. + Defaults to "body". + text: Text to check when check="text_visible". + + Returns: + A boolean for present/visible/text_visible, or an integer count for + count. Missing elements do not cause an exception for these checks. + + Tool selection: + - 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. + + Note: + Except for count's short lookup, this tool does not wait for elements + to appear. Use wait_for when page timing matters. """ sb = _get_sb() + if check == "present": return sb.is_element_present(selector) + if check == "visible": return sb.is_element_visible(selector) + if check == "count": return len(sb.find_elements(selector, timeout=1)) + if check == "text_visible": if text is None: return ( "Error: The 'text_visible' check requires value for 'text'." ) return sb.is_text_visible(text, selector) + return ( f"Error: unknown check '{check}'. " "Use 'present', 'visible', 'count', or 'text_visible'." ) -@mcp.tool() -@handle_sb_errors -def get_all_urls() -> list[str] | str: - """Get a list of all linked URLs (a, link, img, script, meta) on the page. - The full URL with the prefix will be returned for each item in the list. - Eg: `["https://seleniumbase.io", "https://seleniumbase.com"]`. - Depending on the element, the URL could be obtained via "href" or "src". - This is useful for web-crawlers when looking for more links to crawl.""" - return _get_sb().get_all_urls() - - # --------------------------------------------------------------------------- # Interacting with elements # --------------------------------------------------------------------------- @@ -428,74 +653,106 @@ def click( timeout: int | float | None = 7, scroll: bool = True, ) -> str: - """Click an element matched by a CSS selector, or by visible text - (e.g. 'a:contains("Sign in")'). + """Click one or more elements matching a selector. + + This is the primary element-clicking tool. The selector may be a CSS + selector or SeleniumBase text-matching selector such as + 'a:contains("Sign in")'. + Args: - nth: Click only the Nth match (1-indexed), when several elements - match. Takes priority over `all_matches`. (`nth` set to 1 is - no different than a regular click, since the first matching - selector would be clicked.) - all_matches: Click every currently-visible match, in order (e.g. - checking every checkbox on a page). Ignored if `nth` is given. - only_if_visible: Only try the click if the element is visible. - Do nothing (no error or wait) if the element isn't currently - visible, instead of waiting for it or raising an exception. - parent_selector: If set, look for `selector` nested inside parent. - (This option can be used to click on elements inside iframes). - timeout: Seconds to wait for the element when doing a basic click - without nth, all_matches, only_if_visible, or parent_selector. - (Defaults to 7 seconds). - scroll: Whether to scroll the element into view before clicking. + selector: Target CSS selector or text-matching selector. + nth: Click only the Nth matching element, using 1-based indexing. + Takes priority over all_matches. + all_matches: Click every currently visible matching element, in order. + Ignored when nth is provided. + only_if_visible: Attempt the click only when the target is already + visible. Does not wait for the element to become visible. + parent_selector: Restrict the nested lookup to a parent element. + Useful for elements inside iframes or nested containers when + supported by SeleniumBase. + timeout: Seconds to wait for a basic click when no specialized mode + is selected. Defaults to 7 seconds. + scroll: Scroll the target into view before clicking. + + Tool selection: + - Click one matching element -> basic click. + - Click a specific matching occurrence -> set nth. + - Click every visible match -> set all_matches=True. + - Click only when already visible -> set only_if_visible=True. + - Click an element nested inside another element -> set + parent_selector. """ sb = _get_sb() + if nth is not None: if nth < 1: return "Error: nth must be >= 1." sb.click_nth_element(selector, nth, scroll=scroll) return f"Clicked match #{nth} of {selector}" + if all_matches: sb.click_visible_elements(selector) return f"Clicked all visible matches of {selector}" + if only_if_visible: sb.click_if_visible(selector) return f"click (only_if_visible) ran for {selector}" + if parent_selector: sb.nested_click(parent_selector, selector) return f"Clicked {selector} inside {parent_selector}" + sb.click(selector, timeout=timeout, scroll=scroll) return f"Clicked {selector}" @mcp.tool() @handle_sb_errors -def hover(selector: str, then_click_selector: str | None = None) -> str: - """Simulate a mouse hover over an element, - optionally then clicking a second element that the hover reveals - (e.g. an item in a dropdown menu). - This is useful when clicking an item in a dropdown menu that requires - a hover action for the dropdown list of options to appear. +def hover( + selector: str, + then_click_selector: str | None = None, +) -> str: + """Hover over an element, optionally clicking an element revealed by hover. + + Use this for menus, dropdowns, tooltips, or other interfaces where an + element must first be hovered before its target becomes available. + Args: selector: Element to hover over. - then_click_selector: If given, click this element after hovering. + then_click_selector: Optional element to click after the hover. + Useful for a submenu item or dropdown option revealed by hover. + + Returns: + A confirmation describing the hover/click operation. """ sb = _get_sb() + if then_click_selector: sb.hover_and_click(selector, then_click_selector) return f"Hovered {selector} and clicked {then_click_selector}" + sb.hover_element(selector) return f"Hovered {selector}" @mcp.tool() @handle_sb_errors -def drag_and_drop(source_selector: str, target_selector: str) -> str: - """Drag a "draggable" element and drop it onto another element - by simulating mouse actions via the Chrome DevTools Protocol (CDP). - `source_selector` and `target_selector` should be CSS selectors. - A wide range of events are simulated to get the most natural - drag-and-drop effect, which include "pointerdown", "mousedown", - "dragstart", "dragenter", "dragover", "drop", "dragend", "mouseup", - and "pointerup", in that specific order. +def drag_and_drop( + source_selector: str, + target_selector: str, +) -> str: + """Drag a draggable element and drop it onto another element. + + Drag-and-drop is performed through SeleniumBase's CDP browser controls, + simulating the pointer and mouse events expected by web applications. + + Args: + source_selector: CSS selector identifying the draggable source. + target_selector: CSS selector identifying the drop target. + + The simulated interaction includes events such as pointerdown, + mousedown, dragstart, dragenter, dragover, drop, dragend, mouseup, and + pointerup. """ _get_sb().drag_and_drop(source_selector, target_selector) return f"Dragged {source_selector} onto {target_selector}" @@ -515,19 +772,33 @@ def fill_input( ] = "type", timeout: int | float | None = 7, ) -> str: - """Set the value of an input, textarea, or contenteditable element. - If no `timeout` given (0 or None), then SeleniumBase uses 7 seconds. - Raises an exception if the element isn't found within the timeout. + """Enter, append, directly set, or clear text in a form control. + + Use this tool for input elements, textareas, and contenteditable elements. + Args: - mode: 'type' (default) clears the field then types `text`; - 'append' sends `text` as keystrokes without clearing first; - 'set_value' sets the value directly and instantly (good for - sliders and other fast form fills; skips key events); - 'fast_type' clears the field then types `text` fast - (good for when the extra stealth doesn't matter); - 'clear' empties the field — `text` is ignored. + selector: CSS selector or SeleniumBase selector identifying the + input, textarea, or contenteditable element. + text: Text to enter or set. Ignored when mode="clear". + mode: + - "type": Clear the field and type text normally. + - "append": Keep the existing value and send text as keystrokes. + - "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. + 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". """ sb = _get_sb() + if mode == "type": sb.type(selector, text, timeout=timeout) elif mode == "append": @@ -543,6 +814,7 @@ def fill_input( f"Error: unknown mode '{mode}'. " "Use 'type', 'append', 'set_value', 'fast_type', or 'clear'." ) + return f"fill_input(mode={mode!r}) done for {selector}" @@ -553,16 +825,27 @@ def select_option( value: str | int, by: Literal["text", "value", "index"] = "text", ) -> str: - """Select an option in a dropdown. + Args: - value: The option's visible text, its `value` attribute, or its - 0-based index (as a string), depending on `by`. - by: 'text' (default), 'value', or 'index'. - Using "index" for `by` is type-safe. (Eg. 4 and "4" both work the same) + dropdown_selector: CSS selector identifying the elements. For custom JavaScript + dropdowns made from div/button/list elements, use click or other + element-interaction tools instead. """ sb = _get_sb() + if by == "text": sb.select_option_by_text(dropdown_selector, str(value)) elif by == "value": @@ -571,34 +854,46 @@ def select_option( sb.select_option_by_index(dropdown_selector, int(value)) else: return f"Error: unknown by='{by}'. Use 'text', 'value', or 'index'." + return f"Selected ({by}={value!r}) in {dropdown_selector}" @mcp.tool() @handle_sb_errors -def element_action( +def act_on_element( selector: str, - action: Literal["focus", "highlight", "scroll_into_view"] = "focus", + action: Literal[ + "focus", + "highlight", + "scroll_into_view", + ] = "focus", ) -> str: - """Perform a simple positioning/emphasis action on an element. - Raises an exception if the element isn't found within the default - timeout. + """Perform a non-click positioning or debugging action on an element. + + Use this tool when an element needs to be focused, highlighted for human + observation/debugging, or scrolled into the viewport. + + This tool does NOT click, type into, select from, hover over, or otherwise + activate the element. + Args: - action: 'focus' (move keyboard focus to it), 'highlight' (briefly - flash it using JavaScript — useful for narrating actions - on a visible/headed browser), or 'scroll_into_view'. - The "focus" action is useful when an element isn't fully interactable - until the element is in focus. - The "highlight" action is mainly for debugging and demoing purposes. - It briefly changes the border color of an element so that a human - watching the web browser can have their attention be called to - that specific element being highlighted. This "highlight" action - briefly slows the automation during the action. This action may - also reduce stealth on some websites, which may be undesirable. - The "scroll_into_view" action scrolls the page to the element if the - element is not currently in view at the current scroll position. + selector: CSS selector or SeleniumBase selector identifying the target. + action: + - "focus": Move keyboard focus to the element. + - "highlight": Temporarily highlight the element for debugging or + demonstration. This can affect timing and may reduce stealth. + - "scroll_into_view": Scroll the page until the element is in the + current viewport. + + Tool selection: + - Click -> use click. + - Type into a form control -> use fill_input. + - Hover -> use hover. + - Focus, highlight, or scroll without activating -> use + act_on_element. """ sb = _get_sb() + if action == "focus": sb.find_element(selector).focus() elif action == "highlight": @@ -610,6 +905,7 @@ def element_action( f"Error: unknown action '{action}'. " "Use 'focus', 'highlight', or 'scroll_into_view'." ) + return f"{action} done for {selector}" @@ -620,29 +916,54 @@ def element_action( @mcp.tool() @handle_sb_errors def wait_for( - state: Literal["present", "visible", "not_visible", "absent"] = "visible", + state: Literal[ + "present", + "visible", + "not_visible", + "absent", + ] = "visible", selector: str | None = None, text: str | None = None, timeout: int | float | None = 7, ) -> str: - """Wait for an element or text to reach a given state before returning. - If no `timeout` given (0 or None), then SeleniumBase uses 7 seconds. - Raises an exception if the state isn't reached within the timeout. + """Wait until an element or text reaches a requested state. + + 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, + its purpose is synchronization rather than validating a test expectation. + Args: - state: 'present', 'visible', 'not_visible', or 'absent' — describes - what `selector` should reach. Ignored if `text` is given. - selector: Element to wait on. - Defaults to 'body' when waiting on `text`. - text: If given, waits for this text to appear within `selector` - instead of waiting on the element's presence/visibility. - timeout: Seconds to wait. + state: + - "present": Wait until the matching element exists. + - "visible": Wait until the matching element is visible. + - "not_visible": Wait until the matching element is not visible. + - "absent": Wait until the matching element no longer exists. + Ignored when text is provided. + selector: CSS selector or SeleniumBase selector for the element. + Required unless text is supplied. + text: If supplied, wait for this text to appear within selector + (or within "body" when selector is omitted). + timeout: Maximum seconds to wait. Defaults to 7 seconds. + + Returns: + A confirmation when the requested condition is reached. + + 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. """ sb = _get_sb() + if selector is None and text is None: return "Error: `selector` and `text` cannot both be None." + if text is not None: sb.wait_for_text(text, selector or "body", timeout=timeout) return f"Text '{text}' appeared in {selector or 'body'}." + if state == "present": sb.wait_for_element_present(selector, timeout=timeout) elif state == "visible": @@ -656,6 +977,7 @@ def wait_for( f"Error: unknown state '{state}'. " "Use 'present', 'visible', 'not_visible', or 'absent'." ) + return f"Element {selector} reached state '{state}'." @@ -668,41 +990,63 @@ def assert_that( "text", "title", "url", - "url_contains" + "url_contains", ] = "element_visible", selector: str | None = None, expected: str | None = None, exact: bool = False, timeout: int | float | None = 7, ) -> str: - """Assert a condition about the page or an element. Raises an exception - (surfaced back as an error string) if the assertion fails within the - timeout (default 7 seconds). `timeout` applies only to element/text checks. - (For url or title checks, the assertion either passes or fails right away.) + """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. + Args: - check: 'element_present', 'element_visible' (need `selector`); - 'text' (substring of `expected` within `selector`, default - 'html'); 'title', 'url' (exact match), or 'url_contains' - (need `expected`). - selector: Element to check. Used by 'element_present', - 'element_visible', and 'text'. - expected: The text/title/url to check against. Not used for the - element-only checks. - exact: For check='text', require an exact match instead of a - substring match. - timeout: Seconds to wait before failing. + 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. + - "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. + timeout: Maximum seconds to wait for element/text checks. + + Returns: + A confirmation when the expectation passes. + + Raises: + An assertion-related SeleniumBase exception when the expectation + fails; the MCP error wrapper converts it to a descriptive result. + + 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. """ 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: return f"Error: check='{check}' requires value for `expected`." + if check == "element_present": sb.assert_element(selector, timeout=timeout) return f"Confirmed {selector} is present." + if check == "element_visible": sb.assert_element_visible(selector, timeout=timeout) return f"Confirmed {selector} is visible." + if check == "text": target = selector or "html" if exact: @@ -710,15 +1054,19 @@ def assert_that( else: sb.assert_text(expected, target, timeout=timeout) return f"Confirmed text in {target}." + if check == "title": sb.assert_title(expected) return f"Confirmed title is '{expected}'." + if check == "url": sb.assert_url(expected) return f"Confirmed URL is '{expected}'." + if check == "url_contains": sb.assert_url_contains(expected) return f"Confirmed URL contains '{expected}'." + return ( f"Error: unknown check '{check}'. Use 'element_present', " f"'element_visible', 'text', 'title', 'url', or 'url_contains'." @@ -736,59 +1084,64 @@ def manage_cookies( filename: str = "cookies.txt", ) -> Any: """Manage cookies for the current browser session. + Use this tool to inspect, clear, save, or restore browser cookies. Cookie management is useful for inspecting session state, preserving login sessions between browser runs, restoring previously saved sessions, or resetting website state during testing. + Args: - action: The cookie operation to perform: + action: - "get_all": Return all cookies currently available to the browser, - including cookie attributes such as name, value, domain, path, - expiry, and security flags. + including attributes such as name, value, domain, path, expiry, + and security flags. - "clear": Delete all cookies from the current browser session. - Useful for resetting authentication and website state. - - "save": Save the current browser cookies to `filename` so they - can be restored in a later browser session. The file may be + - "save": Save current cookies to filename. The file may be created or overwritten. - - "load": Load cookies from `filename` into the current browser - session. Useful for restoring previously saved authentication or - session state. - filename: Path used by the "save" and "load" actions. Defaults to - "cookies.txt". This argument is ignored for "get_all" and "clear". - The path is passed to SeleniumBase's cookie persistence methods. + - "load": Load cookies from filename into the current browser + session. + filename: Filesystem path used by save/load. Defaults to + "cookies.txt". Ignored for get_all and clear. + Returns: - "get_all": The current browser cookies. - "clear": A confirmation message after cookies are cleared. - "save": A confirmation message containing the destination filename. - "load": A confirmation message containing the source filename. + "get_all": Current browser cookies. + "clear": Confirmation that cookies were cleared. + "save": Confirmation containing the destination filename. + "load": Confirmation containing the source filename. + Security: - Cookie data can contain sensitive authentication credentials, session - identifiers, and other private information. Only inspect, save, load, - or share cookies when explicitly authorized. - `filename` can access the filesystem available to the MCP server. - Use only trusted and authorized file paths. The "save" action may - overwrite an existing file, so avoid unintended or sensitive - destinations. + Cookie data can contain authentication credentials, session + identifiers, and other private information. Only inspect, save, + load, or share cookies when explicitly authorized. + + `filename` is passed to SeleniumBase's cookie persistence methods and + can access the filesystem available to the MCP server. Use only + trusted, authorized paths. The save action may overwrite an existing + file. + Notes: - Cookies belong to the current browser session. Loading previously - saved cookies does not guarantee that a login session will be - restored because cookies may be expired, invalidated, domain-specific, - or dependent on other browser state. Navigate to the appropriate - website before loading cookies when the browser must associate them - with a particular domain. + Loading saved cookies does not guarantee restoration of a login. + Cookies may be expired, invalidated, domain/path restricted, or + dependent on other browser state. Navigate to the relevant site when + necessary so the browser has the appropriate origin for the cookies. """ sb = _get_sb() + if action == "get_all": return sb.get_all_cookies() + if action == "clear": sb.clear_cookies() return "Cookies cleared." + if action == "save": sb.save_cookies(name=filename) return f"Cookies saved to {filename}" + if action == "load": sb.load_cookies(name=filename) return f"Cookies loaded from {filename}" + return ( f"Error: unknown action '{action}'. " "Use 'get_all', 'clear', 'save', or 'load'." @@ -803,37 +1156,46 @@ def manage_storage( storage: Literal["local", "session"] = "local", action: Literal["get", "set"] = "get", ) -> Any: - """Get or set a key in the page's localStorage or sessionStorage. + """Get or set a key in localStorage or sessionStorage. + Args: - key: The localStorage or sessionStorage key to use. - value: The value to set when using 'set' as the `action`. - storage: 'local' (localStorage) or 'session' (sessionStorage). - action: 'get' or 'set'. ('set' requires `value`). - Use this tool for managing localStorage and/or sessionStorage. - Storage is managed via the simple JavaScript calls listed below: - `localStorage.getItem(key)` - `sessionStorage.getItem(key)` - `localStorage.setItem(key, value)` - `sessionStorage.setItem(key, value)` - WARNING: This tool can expose authentication/session secrets that - are are stored in localStorage/sessionStorage on the site. - Only use against trusted sites and with trusted MCP clients. + key: Storage key to read or modify. + value: Value to store when action="set". Required for set. + storage: "local" for localStorage or "session" for sessionStorage. + action: "get" to read the key or "set" to write the key. + + Returns: + The stored value for get, or a confirmation message for set. + + Security: + Web storage can contain authentication tokens, session identifiers, + and other sensitive application state. Only use this tool with trusted + sites and authorized MCP clients. + + Notes: + Storage belongs to the current page origin. Values from one website + are not generally available to another origin. """ sb = _get_sb() + if action not in ("get", "set"): return "Error: action must be 'get' or 'set'." + if action == "set" and value is None: return "Error: value is required when action='set'." + if storage == "local": if action == "get": return sb.get_local_storage_item(key) sb.set_local_storage_item(key, value) return f"Set localStorage[{key!r}]" + if storage == "session": if action == "get": return sb.get_session_storage_item(key) sb.set_session_storage_item(key, value) return f"Set sessionStorage[{key!r}]" + return f"Error: unknown storage '{storage}'. Use 'local' or 'session'." @@ -847,17 +1209,23 @@ def scroll( direction: Literal["up", "down", "top", "bottom"] = "down", amount: int = 25, ) -> str: - """Scroll the page by the given amount or to the location given. + """Scroll the current page vertically. + Args: - direction: 'up' or 'down' (relative, by `amount`), 'top', or 'bottom'. - amount: Relative scroll distance; only used for 'up'/'down'. - When scrolling with `direction` 'up' or 'down', the `amount` represents - a percentage of the window height. (Eg. 'down' with `amount` 25 means - scroll the page down by 25% of the current window height.) If setting the - `direction` to 'top' or 'bottom', then the 'amount' value is ignored. 'top' - scrolls to the top of the page. 'bottom' scrolls to the bottom of the page. + direction: + - "up": Scroll upward by amount percent of the window height. + - "down": Scroll downward by amount percent of the window height. + - "top": Scroll directly to the top; amount is ignored. + - "bottom": Scroll directly to the bottom; amount is ignored. + amount: Percentage of the current viewport height used for relative + up/down scrolling. For example, amount=25 scrolls approximately + one quarter of the viewport height. + + Use act_on_element(action="scroll_into_view") when the goal is to reveal + a specific element rather than scroll the page by a relative amount. """ sb = _get_sb() + if direction == "up": sb.scroll_up(amount=amount) elif direction == "down": @@ -871,6 +1239,7 @@ def scroll( f"Error: unknown direction '{direction}'. " "Use 'up', 'down', 'top', or 'bottom'." ) + return f"Scrolled {direction}." @@ -892,34 +1261,41 @@ def manage_window( width: int | None = None, height: int | None = None, ) -> Any: - """Get or change the browser window's size, position, or state. + """Get or change browser window geometry and state. + Args: - action: 'get_rect', 'set_rect' (requires x, y, width, height), - 'maximize', or 'minimize'. - x: The x position on screen for the 'set_rect' `action` only. - y: The y position on screen for the 'set_rect' `action` only. - width: The window width for the 'set_rect' `action` only. - height: The window height for the 'set_rect' `action` only. - The 'get_rect' action returns the coordinates of the browser window. - The 'set_rect' action lets you set new coordinates for the browser window. - The 'maximize' action maximizes the current browser window. - The 'minimize' action minimizes the current browser window. - The Chrome DevTools Protocol (CDP) is used to change the window state. + action: + - "get_rect": Return the current window coordinates and size. + - "set_rect": Set x, y, width, and height. All four are required. + - "maximize": Maximize the browser window. + - "minimize": Minimize the browser window. + x: Horizontal screen position for set_rect. + y: Vertical screen position for set_rect. + width: Window width for set_rect. + height: Window height for set_rect. + + Use this tool for browser-window geometry/state. For switching between + browser tabs, use manage_tabs instead. """ sb = _get_sb() + if action == "get_rect": return sb.get_window_rect() + if action == "set_rect": if None in (x, y, width, height): return "Error: set_rect requires x, y, width, and height." sb.set_window_rect(x, y, width, height) return f"Window set to ({x}, {y}, {width}x{height})" + if action == "maximize": sb.maximize() return "Window maximized." + if action == "minimize": sb.minimize() return "Window minimized." + return ( f"Error: unknown action '{action}'. " "Use 'get_rect', 'set_rect', 'maximize', or 'minimize'." @@ -941,54 +1317,72 @@ def manage_tabs( switch_to: bool = True, ) -> Any: """List, open, switch between, or close browser tabs. + + Use this tool for tab management. Browser navigation within the current + tab belongs to navigate and navigate_history. + Args: - action: 'list' (returns each open tab's index/url/title — call this - before 'switch' to find the right tab_index), 'open' (a new - tab, optionally navigating to `url`), 'switch' (to `tab_index`), - 'switch_newest', or 'close_active'. - url: Used with action='open'. - tab_index: Used with action='switch'; the index as returned by 'list'. - switch_to: Used with action='open'; whether to switch to the new tab. - Since performing browser actions (such as clicking on a link) can open new - tabs, it is important to keep track of any new tabs opened so that you can - switch to the new tab as needed (or close the active tab). Additionally, - you may want to open a new tab yourself if you need to use multiple tabs - within the same browser session. Listing tabs with the 'list' `action` is - important so that your 'tab_index' for switching tabs is not out-of-range. + action: + - "list": Return each open tab's index, URL, and title. Call this + before switch when you need to determine a tab_index. + - "open": Open a new tab, optionally navigating it to url. + - "switch": Switch to the tab identified by tab_index from list. + - "switch_newest": Switch to the newest tab. + - "close_active": Close the currently active tab. + url: URL for action="open". + tab_index: Index returned by action="list" for action="switch". + switch_to: For action="open", switch to the newly created tab when + True. + + Notes: + Clicking a link or performing another browser action may open a new + tab. Use action="list" to inspect available tabs before switching by + index. Tab indexes should be treated as current-session values and + may change after tabs are opened or closed. """ sb = _get_sb() + if action == "list": tabs = sb.get_tabs() return [ { - "index": i, "url": getattr(t, "url", None), - "title": getattr(t, "title", None) + "index": i, + "url": getattr(t, "url", None), + "title": getattr(t, "title", None), } for i, t in enumerate(tabs) ] + if action == "open": sb.open_new_tab(url=url, switch_to=switch_to) return f"Opened new tab (url={url!r}, switch_to={switch_to})" + if action == "switch": if tab_index is None: return ( "Error: action='switch' requires tab_index " "(see action='list')." ) + tabs = sb.get_tabs() + if tab_index < 0 or tab_index >= len(tabs): return ( f"Error: tab_index={tab_index} out of range. " f"Available indexes: 0-{len(tabs) - 1}." ) + sb.switch_to_tab(tabs[tab_index]) return f"Switched to tab {tab_index}" + if action == "switch_newest": sb.switch_to_newest_tab() return "Switched to newest tab." + if action == "close_active": sb.close_active_tab() return "Closed active tab." + return ( f"Error: unknown action '{action}'. Use 'list', 'open', 'switch', " f"'switch_newest', or 'close_active'." @@ -1002,22 +1396,27 @@ def manage_tabs( @mcp.tool() @handle_sb_errors def solve_captcha() -> str: - """Attempt to solve a CAPTCHA (e.g. Cloudflare Turnstile) on the page. - This tool uses the Chrome DevTools Protocol (CDP) in order to perform - a click (with offset) in order to click the checkbox that's located - in various CAPTCHAs such as Cloudflare Turnstile, reCAPTCHA, and the - FriendlyCaptcha. Since this click is performed with CDP, it is much - stealthier than JavaScript clicks that set 'isTrusted' to False. - Since many of these CAPTCHAs are generally located inside shadow-root - elements, it is not straightforward to determine if the click succeeded. - Therefore, this tool will only state that it attempted to solve a - CAPTCHA, rather than state the result. Since solving a Cloudflare - Turnstile CAPTCHA often results in a "cf_clearance" cookie appearing - within browser cookies, a follow-up to this tool could involve using - the 'manage_cookies' tool to see if that "cf_clearance" cookie appeared - after using this method to solve that CAPTCHA. To determine if there's - a CAPTCHA on the page before using this tool, you could first use the - 'get_page_content' tool to inspect the page HTML for any CAPTCHAs.""" + """Attempt a SeleniumBase CDP-based CAPTCHA interaction. + + This tool attempts to interact with CAPTCHA controls such as Cloudflare + Turnstile, reCAPTCHA, or FriendlyCaptcha using browser/CDP interaction. + + The tool does not guarantee that a CAPTCHA was solved. Some CAPTCHA + controls are embedded inside shadow DOM or otherwise do not expose an + easy success signal. A successful attempt may result in changes to page + state or browser cookies. + + Tool workflow: + 1. Inspect the page with get_content when you need to determine + whether CAPTCHA-related controls are present. + 2. Call solve_captcha to attempt the interaction. + 3. Use get_page_info, get_content, check_state, or manage_cookies + to inspect resulting page/session state. + + Returns: + A message confirming that the CAPTCHA interaction was attempted, not + a guarantee that the CAPTCHA challenge was solved. + """ _get_sb().solve_captcha() return "Attempted CAPTCHA solve." @@ -1031,27 +1430,33 @@ def solve_captcha() -> str: def save_output( format: Literal["screenshot", "html", "pdf"] = "screenshot", filename: str | None = None, - folder: str | None = None + folder: str | None = None, ) -> str: - """Save the current page as a screenshot, HTML source, or PDF file. + """Save the current browser page as a screenshot, HTML file, or PDF. + + Use this tool when an automation workflow needs a persistent artifact + from the current page, such as a screenshot for debugging, page source + for inspection, or a PDF representation. + Args: - format: 'screenshot', 'html', or 'pdf'. - filename: Output filename. Defaults to 'screenshot.png', - 'page_source.html', or 'page.pdf' depending on `format`. - folder: Optional folder to save into. - If 'screenshot' format, then the page is saved as a PNG (.png) file. - If 'html' format, then the page source is saved to an HTML (.html) file. - If 'pdf' format, then the page is saved as a PDF (.pdf) file. - This method is useful for creating artifacts as a result of scraping data - from websites. This is especially important for saving results and data - to your local machine for further processing. (For example, if this MCP - server is being used for automated testing, then if this server finds a - bug on a website, then it can save all the details to the local machine - using any of this tool's supported formats: "screenshot", "html", "pdf".) - SECURITY: `filename`/`folder` can potentially expose filesystem operations - to an MCP client. Existing files could get overwritten during a save. + format: + - "screenshot": Save a PNG screenshot. + - "html": Save the current page source as HTML. + - "pdf": Save the current page as a PDF. + filename: Output filename. Defaults to screenshot.png, + page_source.html, or page.pdf depending on format. + folder: Optional destination folder. + + Returns: + A confirmation containing the output format and filename. + + Security: + filename and folder can affect filesystem paths available to the MCP + server. Existing files may be overwritten. Use trusted, authorized + paths only. """ sb = _get_sb() + if format == "screenshot": name = filename or "screenshot.png" sb.save_screenshot(name, folder=folder) @@ -1066,44 +1471,60 @@ def save_output( f"Error: unknown format '{format}'. " "Use 'screenshot', 'html', or 'pdf'." ) + return f"Saved {format} as {name}" @mcp.tool() @handle_sb_errors def run_javascript(expression: str) -> Any: - """Evaluate a JavaScript expression in the page context and return the - result. This method can run any arbitrary JavaScript on any visited site. - JavaScript evaluation is handled by the 'Runtime.evaluate' function of the - Chrome DevTools Protocol (CDP). When CDP's `Runtime.evaluate` function is - called, it also sets these optional parameters: 'returnByValue' to True, - 'userGesture' to True, 'allowUnsafeEvalBlockedByCSP' to True, and - 'awaitPromise' to True. See the Chrome DevTools Protocol docs for details. - SECURITY: This provides unrestricted JavaScript execution in the browser - context. Only expose this MCP server to trusted clients.""" + """Evaluate arbitrary JavaScript in the current page context. + + Use this only when the required browser operation cannot be accomplished + through the higher-level SeleniumBase tools. + + The expression is evaluated through the Chrome DevTools Protocol + Runtime.evaluate mechanism. Promise results are awaited and values are + returned by value. + + Args: + expression: JavaScript expression to evaluate in the current page + context. + + Returns: + The JavaScript evaluation result when it can be represented across + the MCP boundary. + + Security: + This provides unrestricted JavaScript execution in the current browser + page. It can read or modify page data and interact with the page in + ways that bypass the higher-level tool abstractions. Only expose this + MCP server to trusted clients. + """ return _get_sb().evaluate(expression) @mcp.tool() @handle_sb_errors def wait_seconds(seconds: int | float) -> str: - """Pause script execution for a duration of seconds (int or float). - All this does is call Python `time.sleep(seconds)` in the backend. - It is blocking behavior - no actions are performed while waiting. - Don't use this if you need to call other tools during the wait.""" - _get_sb().sleep(seconds) - return f"Waited {seconds}s" + """Block the MCP server for a fixed number of seconds. + This is a low-level timing tool. It performs no browser action while + waiting and should not be used when waiting for a page condition. -@mcp.tool() -@handle_sb_errors -def get_user_agent() -> str: - """Get the browser's current user agent string.""" - return _get_sb().get_user_agent() + Prefer wait_for when waiting for an element or text to appear/disappear, + because wait_for can return as soon as the requested condition is met. + + Args: + seconds: Number of seconds to block. May be an integer or float. + """ + _get_sb().sleep(seconds) + return f"Waited {seconds}s" def _cleanup_browser(): global _sb + if _sb is not None: try: _sb.quit() @@ -1115,10 +1536,14 @@ def _cleanup_browser(): def main(): atexit.register(_cleanup_browser) print(f'Running the "{mcp.name}" server...', file=sys.stderr) + try: mcp.run(transport="stdio") except (KeyboardInterrupt, SystemExit): - print(f'\nThe "{mcp.name}" server was stopped.', file=sys.stderr) + print( + f'\nThe "{mcp.name}" server was stopped.', + file=sys.stderr, + ) sys.exit(0) diff --git a/mkdocs_build/requirements.txt b/mkdocs_build/requirements.txt index 06921f63faf..4418846ce66 100644 --- a/mkdocs_build/requirements.txt +++ b/mkdocs_build/requirements.txt @@ -1,7 +1,7 @@ # mkdocs dependencies for generating the seleniumbase.io website # Minimum Python version: 3.10 (for generating docs only) -regex>=2026.8.31 +regex>=2026.9.3 pymdown-extensions>=10.21.3 pipdeptree>=4.2.3 python-dateutil>=2.8.2 diff --git a/seleniumbase/__version__.py b/seleniumbase/__version__.py index 982779d0ce8..758eedb57cc 100755 --- a/seleniumbase/__version__.py +++ b/seleniumbase/__version__.py @@ -1,2 +1,2 @@ # seleniumbase package -__version__ = "4.53.2" +__version__ = "4.53.3" diff --git a/seleniumbase/core/sb_cdp.py b/seleniumbase/core/sb_cdp.py index 89b9101e279..308ce7fb3f0 100644 --- a/seleniumbase/core/sb_cdp.py +++ b/seleniumbase/core/sb_cdp.py @@ -827,14 +827,27 @@ def reset_permissions(self): driver = driver.cdp_base return self.loop.run_until_complete(driver.reset_permissions()) - def get_all_urls(self, absolute=True): + def get_all_urls(self, absolute=True, selector=None): """ - Convenience function that returns all links (a,link,img,script,meta). + Convenience function that returns all links (a,link,img,script). :param absolute: Try to build all the links in absolute form instead of "as is", often relative. + :param selector: + Optionally provide a selector to get the HTML from, + instead of the entire page. :return: List of URLs. """ + 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() + soup = self.get_beautiful_soup(html) + page_url = self.get_current_url() + return page_utils._get_unique_links(page_url, soup) return self.loop.run_until_complete( self.page.get_all_urls(absolute=absolute) ) diff --git a/seleniumbase/undetected/cdp_driver/tab.py b/seleniumbase/undetected/cdp_driver/tab.py index 73c4d224f89..82628c42e84 100644 --- a/seleniumbase/undetected/cdp_driver/tab.py +++ b/seleniumbase/undetected/cdp_driver/tab.py @@ -1303,15 +1303,15 @@ async def set_download_path(self, path: PathLike): self._download_behavior = ["allow", str(path.resolve())] async def get_all_linked_sources(self) -> list["element.Element"]: - """Get all elements of tag: link, a, img, scripts meta, video, audio""" + """Get all elements of tag: link, a, img, script""" all_assets = await self.query_selector_all( - selector="a,link,img,script,meta" + selector="a,link,img,script" ) return [element.create(asset, self) for asset in all_assets] async def get_all_urls(self, absolute=True) -> list[str]: """ - Convenience function, which returns all links (a,link,img,script,meta). + Convenience function, which returns all links (a,link,img,script). :param absolute: Try to build all the links in absolute form instead of "as is", often relative. @@ -1321,7 +1321,7 @@ async def get_all_urls(self, absolute=True) -> list[str]: res = [] all_assets = await self.query_selector_all( - selector="a,link,img,script,meta" + selector="a,link,img,script" ) for asset in all_assets: if not absolute: diff --git a/setup.py b/setup.py index 59cff37bdc9..a4608e0f1aa 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.5" + "uv>=0.12.8" ], }, packages=[