diff --git a/mcp_servers/README.md b/mcp_servers/README.md index 401a4709855f..d53fd96dbfa7 100644 --- a/mcp_servers/README.md +++ b/mcp_servers/README.md @@ -91,27 +91,27 @@ Restart Claude Desktop. You should see a 🔨 tools icon indicating the server c * `start_browser` * `close_browser` -* `navigate` +* `goto_url` * `manage_history` * `get_page_info` * `find_elements` * `get_content` * `get_attributes` * `check_condition` -* `click` +* `click_element` * `hover_action` * `type_text` * `select_option` -* `focus` +* `focus_element` * `wait_for` * `assert_condition` * `manage_cookies` * `manage_storage` -* `scroll` +* `scroll_page` * `manage_window` * `manage_tabs` * `solve_captcha` -* `save_output` +* `save_page` * `run_javascript` ## 4. Connect it to Claude Code @@ -152,25 +152,25 @@ Most tools accept a `selector` argument. Behavior varies slightly by tool, so ch - **CSS selectors** are preferred and supported by every tool that takes a selector. - **XPath** is accepted by several (not all) tools. Some tools go through SeleniumBase's XPath-to-CSS conversion first; expressions that can't be converted (e.g. `contains(...)`) aren't supported by those tools. -- **SeleniumBase's visible-text selector** syntax, e.g. `a:contains("Sign in")`, is accepted by several tools (including `click`, when not using `all_matches`) but not all of them — `find_elements`, for example, only supports CSS/XPath. +- **SeleniumBase's visible-text selector** syntax, e.g. `a:contains("Sign in")`, is accepted by several tools (including `click_element`, when not using `all_matches`) but not all of them — `find_elements`, for example, only supports CSS/XPath. ## Tools exposed -Tools here are grouped around a shared `selector` convention. Several near-identical one-off tools (e.g. separate click/hover/drag/wait/cookie/storage variants) have been consolidated into a single tool with a `mode`/`action`/`state`/`check` parameter, so there are fewer near-neighbor tools to disambiguate between while every underlying capability stays available. +Tools here are grouped around a shared `selector` convention. Several near-identical one-off tools (e.g. separate click/hover/drag/wait/cookie/storage variants) have been consolidated into a single tool with a `mode`/`action`/`state`/`check` parameter, so there are fewer near-neighbor tools to disambiguate between while every underlying capability stays available. Tool names also follow a verb+object convention (`click_element`, `focus_element`, `scroll_page`, `save_page`, `goto_url`) rather than bare verbs, so a tool's name signals what it acts on without needing to read its description. | Group | Tool(s) | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | Session | `start_browser(url, headless, use_chromium, browser_executable_path, incognito, guest, ad_block, proxy)`, `close_browser` | -| Navigation | `navigate`, `manage_history(action: back/forward/reload/list)`, `get_page_info` (running status, url, title, origin, user agent in one call) | +| Navigation | `goto_url`, `manage_history(action: back/forward/reload/list)`, `get_page_info` (running status, url, title, origin, user agent in one call) | | Finding & reading | `find_elements(selector, timeout, include_html)`, `get_content(selector, output_format: text/html/urls, timeout)`, `get_attributes(selector, attribute, timeout)`, `check_condition(check: present/visible, text)` | -| Interacting | `click(selector, nth, all_matches, only_if_visible, parent_selector, timeout, scroll)`, `hover_action(selector1, selector2, action: none/click/drag_and_drop)`, `type_text(mode: fill_input/append/fast_type/set_value/clear_only)`, `select_option(by: text/value/index)`, `focus(action: scroll_to_element/focus/highlight, timeout)` | +| 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, timeout)` | | Waiting | `wait_for(state: present/visible/not_visible/absent/seconds_passed, text)` | | Assertions | `assert_condition(check: element_present/element_visible/text_visible/title/url/url_contains)` | | Cookies & storage | `manage_cookies(action: get_all/clear/save/load)`, `manage_storage(storage: local/session, action: get/set)` | -| Scrolling | `scroll(direction: up/down/top/bottom, amount)` | +| Scrolling | `scroll_page(direction: up/down/top/bottom, amount)` | | Windows & tabs | `manage_window(action: get_rect/set_rect/maximize/minimize)`, `manage_tabs(action: list/open/switch/switch_newest/close_active)` | | Captcha | `solve_captcha` | -| Output & misc | `save_output(format: screenshot/html/pdf)`, `run_javascript` | +| Output & misc | `save_page(format: screenshot/html/pdf)`, `run_javascript` | ## Design notes / things to adapt for your use case @@ -180,15 +180,17 @@ Tools here are grouped around a shared `selector` convention. Several near-ident - **`start_browser` retries once before failing.** If the first launch attempt raises, it's retried once automatically before returning an error. This was added after seeing occasional first-attempt failures when testing against Glama's MCP Inspector; it costs nothing on the common case where the first launch already succeeds. -- **Two error-handling paths, by design.** Most failures (a selector isn't found, an assertion fails, an invalid `action`/`mode`/`check` value is passed) are caught by the `handle_sb_errors` decorator and returned as a descriptive string, e.g. `Error in click: NoSuchElementException - ...`, so the calling agent can read the failure and self-correct. There's one deliberate exception: calling any tool other than `start_browser`/`close_browser` when no browser session is running raises `ToolError` (via the shared `_get_sb()` helper) instead of returning a string. `handle_sb_errors` explicitly re-raises `ToolError` rather than catching it, so this surfaces to the MCP client as a real tool-call error (`is_error=True`), not as ordinary text the agent has to pattern-match on. `start_browser` and `close_browser` handle their own lifecycle errors directly (e.g. "already running", a failed `quit()`) and also return strings rather than raising. +- **Several tools were renamed from bare verbs to verb+object names.** `navigate` → `goto_url`, `click` → `click_element`, `focus` → `focus_element`, `scroll` → `scroll_page`, `save_output` → `save_page`. Behavior is unchanged in every case — these are pure renames for clarity, so a tool's name alone signals what it acts on (a page, an element, a URL) instead of reading as a generic action that could apply to anything. + +- **Two error-handling paths, by design.** Most failures (a selector isn't found, an assertion fails, an invalid `action`/`mode`/`check` value is passed) are caught by the `handle_sb_errors` decorator and returned as a descriptive string, e.g. `Error in click_element: NoSuchElementException - ...`, so the calling agent can read the failure and self-correct. There's one deliberate exception: calling any tool other than `start_browser`/`close_browser` when no browser session is running raises `ToolError` (via the shared `_get_sb()` helper) instead of returning a string. `handle_sb_errors` explicitly re-raises `ToolError` rather than catching it, so this surfaces to the MCP client as a real tool-call error (`is_error=True`), not as ordinary text the agent has to pattern-match on. `start_browser` and `close_browser` handle their own lifecycle errors directly (e.g. "already running", a failed `quit()`) and also return strings rather than raising. - **No standalone session-status tool.** There is no separate `browser_status`-style tool. `get_page_info` doubles as the status check: it returns `{"running": False}` (optionally with an `error` field) when there's no active session or the session errors out, and page metadata (`running: True`, `url`, `title`, `origin`, `user_agent`) otherwise. `get_page_info` does not include navigation history — that lives on `manage_history(action="list")` instead (see below). -- **Navigation and history live in one tool: `manage_history`.** What used to be `navigate_history` is now `manage_history`, and it gained a fourth action: `"list"`, which returns the browser's navigation history as `{"position": <0-indexed current entry>, "entries": [...]}`, where each entry has `id`, `url`, `user_typed_url`, `title`, and `transition_type`. `"back"`, `"forward"`, and `"reload"` behave as before. This is the only way to retrieve navigation history now — `get_page_info` doesn't return it. +- **Navigation and history live in one tool: `manage_history`.** What used to be `navigate_history` is now `manage_history`, and it gained a fourth action: `"list"`, which returns the browser's navigation history as `{"position": <0-indexed current entry>, "entries": [...]}`, where each entry has `id`, `url`, `user_typed_url`, `title`, and `transition_type`. `"back"`, `"forward"`, and `"reload"` behave as before. This is the only way to retrieve navigation history now — `get_page_info` doesn't return it. Use `goto_url` for navigating to an arbitrary URL rather than moving through existing history. - **`get_content` always reads from an element, not the whole document.** `selector` now defaults to `"body"` rather than `None`/whole-page, and there's no `include_shadow_dom` option anymore — `get_content` no longer calls `get_page_source` at all. `output_format="html"` returns a single element's outer HTML (`get_element_html`), and `output_format="urls"` returns URLs discovered within that element (`get_all_urls(selector=...)`), rather than the full raw page source including shadow roots. If you need the complete page source (shadow DOM included), that capability isn't exposed by any tool here currently. `get_content` also gained a `timeout` parameter (default 5s) for waiting on the target element. -- **`get_attributes` and `focus` now take a `timeout`.** Both default to 5 seconds and wait for the target element the same way most other interaction tools do; previously neither exposed a timeout. +- **`get_attributes` and `focus_element` now take a `timeout`.** Both default to 5 seconds and wait for the target element the same way most other interaction tools do; previously neither exposed a timeout. - **`check_condition` is deliberately narrow.** Its `check` parameter only accepts `"present"` or `"visible"` — there's no built-in `"count"` check anymore; call `find_elements` and read the returned `count` field instead. Passing `text` checks whether that text is visible within `selector` and takes priority over `check` when both are given — so `check_condition(text="Sign in")` behaves differently from `check_condition(check="visible")`, not as two variants of the same check. Note that an empty string for `text` (or for `wait_for`'s `selector`/`text`) is treated as not provided, since both tools now branch on truthiness rather than on `is not None`. @@ -196,17 +198,17 @@ Tools here are grouped around a shared `selector` convention. Several near-ident - **`wait_seconds` was folded into `wait_for`.** There's no standalone `wait_seconds` tool anymore. Use `wait_for(state="seconds_passed", timeout=)` instead — it ignores `selector`/`text` and blocks for the full `timeout` duration. All other `wait_for` states behave as before. -- **Hover, click-after-hover, and drag-and-drop share one tool.** `hover_action(selector1, selector2, action)` replaces the earlier separate `hover` and `drag_and_drop` tools. `action="none"` hovers `selector1` only; `action="click"` hovers `selector1` then clicks `selector2` (useful for dropdown/submenu items revealed by hovering); `action="drag_and_drop"` drags `selector1` onto `selector2`. (`selector2` is required when `action` is `"click"` or `"drag_and_drop"`.) +- **Hover, click-after-hover, and drag-and-drop share one tool.** `hover_action(selector1, selector2, action)` replaces the earlier separate `hover` and `drag_and_drop` tools. `action="hover"` (the default) hovers `selector1` only; `action="hover_and_click"` hovers `selector1` then clicks `selector2` (useful for dropdown/submenu items revealed by hovering); `action="drag_and_drop"` drags `selector1` onto `selector2`. (`selector2` is required when `action` is `"hover_and_click"` or `"drag_and_drop"`.) Note the action names themselves changed from an earlier `none`/`click`/`drag_and_drop` scheme — `"hover"` replaces `"none"` as the default/simple-hover value, and `"hover_and_click"` replaces the bare `"click"` to avoid confusion with the unrelated `click_element` tool. -- **Non-activating element actions are `focus`.** What used to be `act_on_element` is now `focus(selector, action, timeout)`, with actions `scroll_to_element` (the default), `focus`, and `highlight` — note the default action is scrolling the element into view, not focusing it. None of these actions click, type into, select from, or otherwise activate the element; use `click`, `type_text`, `select_option`, or `hover_action` for that. +- **Non-activating element actions are `focus_element`.** What used to be `act_on_element` (then `focus`) is now `focus_element(selector, action, timeout)`, with actions `scroll_to_element` (the default), `focus`, and `highlight` — note the default action is scrolling the element into view, not focusing it. None of these actions click, type into, select from, or otherwise activate the element; use `click_element`, `type_text`, `select_option`, or `hover_action` for that. -- **`scroll`'s `amount` isn't capped at 100.** Relative up/down scrolling by more than 100% of the viewport height is allowed (e.g. `amount=200` scrolls roughly two viewport heights); negative amounts are rejected for `"up"`/`"down"`. +- **`scroll_page`'s `amount` isn't capped at 100.** Relative up/down scrolling by more than 100% of the viewport height is allowed (e.g. `amount=200` scrolls roughly two viewport heights); negative amounts are rejected for `"up"`/`"down"`. -- **Elements don't cross the wire as handles.** In native CDP Mode, `find_element()` returns a live object with its own methods (`el.click()`, `el.get_html()`, ...). MCP tools can only return JSON-serializable data, so `find_elements` resolves each match immediately to a plain dict (`tag_name`, `text`, and optionally `html`) instead of returning a handle you could call further methods on. If you need to act on one of several matches, use `click(selector, nth=...)` (acts by position) rather than "find, then click" as two separate steps. +- **Elements don't cross the wire as handles.** In native CDP Mode, `find_element()` returns a live object with its own methods (`el.click()`, `el.get_html()`, ...). MCP tools can only return JSON-serializable data, so `find_elements` resolves each match immediately to a plain dict (`tag_name`, `text`, and optionally `html`) instead of returning a handle you could call further methods on. If you need to act on one of several matches, use `click_element(selector, nth=...)` (acts by position) rather than "find, then click" as two separate steps. - **CAPTCHA-solving.** `solve_captcha` attempts to detect and interact with several challenge types over CDP (e.g. Cloudflare Turnstile, reCAPTCHA, hCaptcha, DataDome Slider, FriendlyCaptcha), including slider-style drag interactions, without guaranteeing success. -- **Security.** `run_javascript` runs arbitrary JS, and `manage_storage` can expose authentication/session secrets; `manage_cookies` and `save_output` accept filenames/folders that can touch the filesystem. This server can also drive a real browser to real sites — don't expose it over an untrusted network transport; stdio + local trust (the default here) is the safe setup. +- **Security.** `run_javascript` runs arbitrary JS, and `manage_storage` can expose authentication/session secrets; `manage_cookies` and `save_page` accept filenames/folders that can touch the filesystem. This server can also drive a real browser to real sites — don't expose it over an untrusted network transport; stdio + local trust (the default here) is the safe setup. ## Extending diff --git a/mcp_servers/pyproject.toml b/mcp_servers/pyproject.toml index 1369694b0f67..8298c72f75c0 100644 --- a/mcp_servers/pyproject.toml +++ b/mcp_servers/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "seleniumbase-mcp" -version = "1.3.2dev0" +version = "1.3.3dev0" description = "MCP server exposing SeleniumBase CDP Mode as tools for MCP clients." readme = "README.md" requires-python = ">=3.10" diff --git a/mcp_servers/server.py b/mcp_servers/server.py index 4b5e6c5bf19f..6a355ade15df 100644 --- a/mcp_servers/server.py +++ b/mcp_servers/server.py @@ -34,7 +34,8 @@ Tool-selection philosophy: - Use 'start_browser'/'close_browser' for opening/quitting the web browser. -- Use 'navigate'/'manage_history' for browser navigation & history inspection. +- Use 'goto_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 @@ -43,11 +44,12 @@ - Use 'wait_for' 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'/'type_text'/'select_option' for standard page interactions. +- Use 'click_element'/'type_text'/'select_option' for standard page + interactions. - Use 'hover_action' for just a hover, with a click, or with a drag/drop. -- Use 'focus' for element positioning and visual focus. +- Use 'focus_element' for element positioning and visual focus. - Use 'solve_captcha' for clicking the checkbox of a CAPTCHA on the page. -- Use 'save_output' for saving page output as a PNG, a PDF, or an HTML file. +- Use 'save_page' for saving page output as a PNG, a PDF, or an HTML file. """ from __future__ import annotations import atexit @@ -109,8 +111,8 @@ def start_browser( ) -> str: """Launch a persistent SeleniumBase Pure CDP Mode browser session. - Call this before using browser interaction tools such as navigate, - get_content, click, type_text, or find_elements. The same browser + Call this before using browser interaction tools such as goto_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. @@ -336,7 +338,7 @@ def get_page_info() -> dict[str, Any]: @mcp.tool() @handle_sb_errors -def navigate(url: str) -> str: +def goto_url(url: str) -> str: """Navigate the current browser tab to a URL. Use this when the browser needs to visit a new URL rather than move @@ -358,7 +360,7 @@ def navigate(url: str) -> str: A confirmation message containing the requested URL. Tool selection: - - Go to a new URL -> use navigate. + - Go to a new URL -> use goto_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"). @@ -377,7 +379,7 @@ def manage_history( Use 'back' or 'forward' for history navigation, 'reload' to refresh while bypassing the cache, or 'list' to inspect history. - Use 'navigate' for an arbitrary URL. + Use 'goto_url' for navigation to an arbitrary URL. Args: action: @@ -473,7 +475,7 @@ def find_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 click one of several matches -> use click_element with nth. - Need to know whether an element is present/visible -> use check_condition. @@ -524,6 +526,7 @@ def get_content( Args: selector: CSS selector or SeleniumBase-supported XPath selector. + Default: "body". output_format: - "text": Return visible text from the selected element. @@ -532,18 +535,18 @@ def get_content( selected element. Returned URLs are normalized to full URLs with their protocol prefixes. - timeout: Maximum seconds to wait for the target element. Default: 5. Tool selection: - Need URL, title, origin, or User-Agent -> 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 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. + + If there's no matching element found within the timeout, + then @handle_sb_errors returns details from the exception raised. """ sb = _get_sb() @@ -583,9 +586,6 @@ def get_attributes( timeout: Maximum seconds to wait for the target element. Default: 5. - Returns: - The requested attribute(s). - Tool selection: - Need one or more HTML attribute values from a specific element -> use this tool. @@ -597,7 +597,7 @@ def get_attributes( This is a read-only operation. If there's no matching element found within the timeout, - then @handle_sb_errors will return details from the exception raised. + then @handle_sb_errors returns details from the exception raised. """ sb = _get_sb() @@ -680,7 +680,7 @@ def check_condition( @mcp.tool() @handle_sb_errors -def click( +def click_element( selector: str, nth: int | None = None, all_matches: bool = False, @@ -733,11 +733,14 @@ def click( indexed click. Default: True. Examples: - - Click one element: `click("button.submit")` - - Click the 2nd matching element: `click("button", nth=2)` - - Click all visible matches: `click(".dismiss", all_matches=True)` - - Click only if already visible: `click("#menu", only_if_visible=True)` - - Click inside a container: `click(".item", parent_selector="#result")` + - Click one element: `click_element("button.submit")` + - Click the 2nd matching element: `click_element("button", nth=2)` + - Click all visible matches: + `click_element(".dismiss", all_matches=True)` + - Click only if already visible: + `click_element("#menu", only_if_visible=True)` + - Click inside a container: + `click_element(".item", parent_selector="#result")` """ sb = _get_sb() @@ -768,11 +771,7 @@ def click( def hover_action( selector1: str, selector2: str | None = None, - action: Literal[ - "none", - "click", - "drag_and_drop", - ] = "none", + action: Literal["hover", "hover_and_click", "drag_and_drop"] = "hover", ) -> str: """Hover over an element, optionally click another, or drag-and-drop. @@ -782,46 +781,39 @@ def hover_action( Args: selector1: The primary element selector. - For action="none", this is the element to hover over. - For action="click", this is the element to hover over before - clicking selector2. + 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. For action="drag_and_drop", this is the draggable source element. selector2: The secondary element selector. - Required for action="click", where it identifies the element - revealed or targeted after hovering selector1. + Required for action="hover_and_click", where it identifies + the element revealed or targeted after hovering selector1. Required for action="drag_and_drop", where it identifies the destination/drop target. - Not used for action="none". + Not used for action="hover". action: - - "none": Hover over selector1 only. - - "click": Hover over selector1, then click selector2. + - "hover": Hover over selector1 only. + - "hover_and_click": Hover over selector1, then click selector2. - "drag_and_drop": Drag selector1 and drop it onto selector2. Returns: A confirmation message describing the performed operation. Tool selection: - - Simple hover -> action="none". - - Hover over one element and then click another -> action="click". + - Simple hover -> action="hover". + - Hover over one element and click another -> action="hover_and_click". - Drag one element onto another -> action="drag_and_drop". - - Notes: - For action="click", selector1 is the hover target and selector2 is - the click target. - - For action="drag_and_drop", selector1 is the source and selector2 - is the destination. """ sb = _get_sb() - if action == "none": + if action == "hover": sb.hover_element(selector1) return f"Hovered {selector1}" - if action == "click": + if action == "hover_and_click": if selector2 is None: return "Error: action='click' requires selector2." sb.hover_and_click(selector1, selector2) @@ -835,7 +827,7 @@ def hover_action( return ( f"Error: unknown action '{action}'. " - "Use 'none', 'click', or 'drag_and_drop'." + "Use 'hover', 'hover_and_click', or 'drag_and_drop'." ) @@ -934,8 +926,8 @@ def select_option( An error when the dropdown or requested option cannot be found. This tool is for native