seleniumbase-mcp 0.1.0a1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- seleniumbase_mcp-0.1.0a1/PKG-INFO +12 -0
- seleniumbase_mcp-0.1.0a1/README.md +1 -0
- seleniumbase_mcp-0.1.0a1/cdp_server.py +689 -0
- seleniumbase_mcp-0.1.0a1/driver_server.py +249 -0
- seleniumbase_mcp-0.1.0a1/pyproject.toml +27 -0
- seleniumbase_mcp-0.1.0a1/sb_server.py +758 -0
- seleniumbase_mcp-0.1.0a1/seleniumbase_mcp.egg-info/PKG-INFO +12 -0
- seleniumbase_mcp-0.1.0a1/seleniumbase_mcp.egg-info/SOURCES.txt +12 -0
- seleniumbase_mcp-0.1.0a1/seleniumbase_mcp.egg-info/dependency_links.txt +1 -0
- seleniumbase_mcp-0.1.0a1/seleniumbase_mcp.egg-info/entry_points.txt +4 -0
- seleniumbase_mcp-0.1.0a1/seleniumbase_mcp.egg-info/requires.txt +3 -0
- seleniumbase_mcp-0.1.0a1/seleniumbase_mcp.egg-info/top_level.txt +3 -0
- seleniumbase_mcp-0.1.0a1/setup.cfg +8 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: seleniumbase-mcp
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: MCP servers exposing SeleniumBase (Driver, Pure CDP Mode, and SB()) as tools for MCP clients like Claude Desktop and Claude Code.
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: mcp<3.0.0,>=2.0.0
|
|
9
|
+
Requires-Dist: seleniumbase>=4.52.1
|
|
10
|
+
Requires-Dist: typer>=0.27.1
|
|
11
|
+
|
|
12
|
+
# seleniumbase-mcp
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# seleniumbase-mcp
|
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
SeleniumBase Pure CDP Mode MCP Server
|
|
4
|
+
======================================
|
|
5
|
+
Exposes SeleniumBase's Pure CDP Mode (sync API, `seleniumbase.sb_cdp.Chrome`)
|
|
6
|
+
as MCP tools. Pure CDP Mode drives the browser entirely over the Chrome
|
|
7
|
+
DevTools Protocol (no WebDriver), which is SeleniumBase's stealthiest mode
|
|
8
|
+
and includes captcha-solving support.
|
|
9
|
+
|
|
10
|
+
Reference:
|
|
11
|
+
github.com/seleniumbase/SeleniumBase/blob/master/help_docs/cdp_mode_methods.md
|
|
12
|
+
|
|
13
|
+
Model: one persistent `sb_cdp.Chrome` session per server process. Call
|
|
14
|
+
start_browser once, drive it with the other tools, then close_browser.
|
|
15
|
+
|
|
16
|
+
Note on elements: CDP-mode element objects (from find_element/find_all) are
|
|
17
|
+
live handles with their own methods (.click(), .get_html(), ...) that can't
|
|
18
|
+
cross the MCP boundary as stateful objects. Tools here resolve an element
|
|
19
|
+
immediately to a plain dict (tag, text, html) rather than returning a handle.
|
|
20
|
+
If you need to act on a *specific* one of several matching elements, use
|
|
21
|
+
click_nth_element / click_nth_visible_element rather than find + click.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from mcp.server import MCPServer
|
|
27
|
+
from seleniumbase import sb_cdp
|
|
28
|
+
|
|
29
|
+
mcp = MCPServer("seleniumbase-cdp")
|
|
30
|
+
|
|
31
|
+
_sb: sb_cdp.CDPMethods | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _get_sb() -> sb_cdp.CDPMethods:
|
|
35
|
+
if _sb is None:
|
|
36
|
+
raise RuntimeError("No browser session. Call start_browser first.")
|
|
37
|
+
return _sb
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# Session lifecycle
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
@mcp.tool()
|
|
45
|
+
def start_browser(
|
|
46
|
+
url: str | None = None,
|
|
47
|
+
headless: bool = False,
|
|
48
|
+
incognito: bool = False,
|
|
49
|
+
guest: bool = False,
|
|
50
|
+
proxy: str | None = None,
|
|
51
|
+
ad_block: bool = False,
|
|
52
|
+
) -> str:
|
|
53
|
+
"""Launch a Pure CDP Mode browser session. Must be called before any
|
|
54
|
+
other tool. The browser is driven entirely over CDP (no WebDriver),
|
|
55
|
+
which is SeleniumBase's most stealth/bot-detection-resistant mode.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
url: Optional URL to open immediately on launch.
|
|
59
|
+
headless: Run without a visible window.
|
|
60
|
+
incognito: Launch in a private/incognito window.
|
|
61
|
+
guest: Launch in Chrome guest mode.
|
|
62
|
+
proxy: Proxy string, e.g. "USER:PASS@SERVER:PORT" or "SERVER:PORT".
|
|
63
|
+
ad_block: Block ads.
|
|
64
|
+
"""
|
|
65
|
+
global _sb
|
|
66
|
+
if _sb is not None:
|
|
67
|
+
return (
|
|
68
|
+
"A browser session is already running. Call close_browser first."
|
|
69
|
+
)
|
|
70
|
+
kwargs: dict[str, Any] = {"headless": headless}
|
|
71
|
+
if incognito:
|
|
72
|
+
kwargs["incognito"] = True
|
|
73
|
+
if guest:
|
|
74
|
+
kwargs["guest"] = True
|
|
75
|
+
if proxy:
|
|
76
|
+
kwargs["proxy"] = proxy
|
|
77
|
+
if ad_block:
|
|
78
|
+
kwargs["ad_block"] = True
|
|
79
|
+
_sb = sb_cdp.Chrome(url, **kwargs)
|
|
80
|
+
return f"Started Pure CDP Mode browser (url={url!r}, headless={headless})"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@mcp.tool()
|
|
84
|
+
def close_browser() -> str:
|
|
85
|
+
"""Close the browser and end the session."""
|
|
86
|
+
global _sb
|
|
87
|
+
if _sb is None:
|
|
88
|
+
return "No browser session was running."
|
|
89
|
+
_sb.quit()
|
|
90
|
+
_sb = None
|
|
91
|
+
return "Browser closed."
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
# Navigation
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
@mcp.tool()
|
|
99
|
+
def navigate(url: str) -> str:
|
|
100
|
+
"""Navigate to a URL."""
|
|
101
|
+
_get_sb().get(url)
|
|
102
|
+
return f"Navigated to {url}"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@mcp.tool()
|
|
106
|
+
def reload_page(ignore_cache: bool = True) -> str:
|
|
107
|
+
"""Reload the current page."""
|
|
108
|
+
_get_sb().reload(ignore_cache=ignore_cache)
|
|
109
|
+
return "Page reloaded."
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@mcp.tool()
|
|
113
|
+
def go_back() -> str:
|
|
114
|
+
"""Go back one page in browser history."""
|
|
115
|
+
_get_sb().go_back()
|
|
116
|
+
return "Navigated back."
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@mcp.tool()
|
|
120
|
+
def go_forward() -> str:
|
|
121
|
+
"""Go forward one page in browser history."""
|
|
122
|
+
_get_sb().go_forward()
|
|
123
|
+
return "Navigated forward."
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@mcp.tool()
|
|
127
|
+
def get_navigation_history() -> Any:
|
|
128
|
+
"""Get the browser's navigation history."""
|
|
129
|
+
return _get_sb().get_navigation_history()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@mcp.tool()
|
|
133
|
+
def get_current_url() -> str:
|
|
134
|
+
"""Get the URL of the current page."""
|
|
135
|
+
return _get_sb().get_current_url()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@mcp.tool()
|
|
139
|
+
def get_title() -> str:
|
|
140
|
+
"""Get the title of the current page."""
|
|
141
|
+
return _get_sb().get_title()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@mcp.tool()
|
|
145
|
+
def get_origin() -> str:
|
|
146
|
+
"""Get the origin (scheme + host) of the current page."""
|
|
147
|
+
return _get_sb().get_origin()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
# Finding & reading
|
|
152
|
+
# ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
@mcp.tool()
|
|
155
|
+
def find_element_info(
|
|
156
|
+
selector: str, best_match: bool = False, timeout: int | None = None
|
|
157
|
+
) -> dict:
|
|
158
|
+
"""Find one element and return its tag name, text, and outer HTML.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
selector: CSS selector, or text to search for (CDP mode can match
|
|
162
|
+
elements by visible text as well as by selector).
|
|
163
|
+
best_match: When matching by text and multiple elements qualify,
|
|
164
|
+
pick the one whose text length is closest to the search text.
|
|
165
|
+
timeout: Seconds to wait for the element to appear.
|
|
166
|
+
"""
|
|
167
|
+
el = _get_sb().find_element(
|
|
168
|
+
selector, best_match=best_match, timeout=timeout
|
|
169
|
+
)
|
|
170
|
+
return {"tag_name": el.tag_name, "text": el.text, "html": el.get_html()}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@mcp.tool()
|
|
174
|
+
def find_all_info(selector: str, timeout: int | None = None) -> list[dict]:
|
|
175
|
+
"""Find all matching elements and return tag name + text for each."""
|
|
176
|
+
els = _get_sb().find_all(selector, timeout=timeout)
|
|
177
|
+
return [{"tag_name": e.tag_name, "text": e.text} for e in els]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@mcp.tool()
|
|
181
|
+
def get_text(selector: str = "body") -> str:
|
|
182
|
+
"""Get the visible text within an element (default: whole page body)."""
|
|
183
|
+
return _get_sb().get_text(selector)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@mcp.tool()
|
|
187
|
+
def get_html_source(include_shadow_dom: bool = True) -> str:
|
|
188
|
+
"""Get the full HTML source of the current page."""
|
|
189
|
+
return _get_sb().get_page_source(include_shadow_dom=include_shadow_dom)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@mcp.tool()
|
|
193
|
+
def get_element_html(selector: str) -> str:
|
|
194
|
+
"""Get the outer HTML of a specific element."""
|
|
195
|
+
return _get_sb().get_element_html(selector)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@mcp.tool()
|
|
199
|
+
def get_element_attribute(selector: str, attribute: str) -> Any:
|
|
200
|
+
"""Get one attribute's value from an element."""
|
|
201
|
+
return _get_sb().get_element_attribute(selector, attribute)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@mcp.tool()
|
|
205
|
+
def get_element_attributes(selector: str) -> dict:
|
|
206
|
+
"""Get all attributes of an element as a dict."""
|
|
207
|
+
return _get_sb().get_element_attributes(selector)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@mcp.tool()
|
|
211
|
+
def find_elements_count(selector: str, timeout: int | None = None) -> int:
|
|
212
|
+
"""Count how many elements on the page match a selector."""
|
|
213
|
+
return len(_get_sb().find_elements(selector, timeout=timeout))
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@mcp.tool()
|
|
217
|
+
def is_element_present(selector: str) -> bool:
|
|
218
|
+
"""Check whether an element matching a selector exists in the DOM."""
|
|
219
|
+
return _get_sb().is_element_present(selector)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@mcp.tool()
|
|
223
|
+
def is_element_visible(selector: str) -> bool:
|
|
224
|
+
"""Check whether an element matching a selector is visible."""
|
|
225
|
+
return _get_sb().is_element_visible(selector)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@mcp.tool()
|
|
229
|
+
def is_text_visible(text: str, selector: str = "body") -> bool:
|
|
230
|
+
"""Check whether specific text is visible within an element."""
|
|
231
|
+
return _get_sb().is_text_visible(text, selector)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@mcp.tool()
|
|
235
|
+
def get_all_urls(absolute: bool = True) -> list[str]:
|
|
236
|
+
"""Get all linked URLs (a, link, img, script, meta) on the page."""
|
|
237
|
+
return _get_sb().get_all_urls(absolute=absolute)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# ---------------------------------------------------------------------------
|
|
241
|
+
# Interacting with elements
|
|
242
|
+
# ---------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
@mcp.tool()
|
|
245
|
+
def click(
|
|
246
|
+
selector: str, timeout: int | None = None, scroll: bool = True
|
|
247
|
+
) -> str:
|
|
248
|
+
"""Click an element matched by a CSS selector (or by text, e.g.
|
|
249
|
+
'a:contains("Sign in")')."""
|
|
250
|
+
_get_sb().click(selector, timeout=timeout, scroll=scroll)
|
|
251
|
+
return f"Clicked {selector}"
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@mcp.tool()
|
|
255
|
+
def click_if_visible(selector: str, timeout: int = 0) -> str:
|
|
256
|
+
"""Click an element only if it's currently visible; no-op otherwise."""
|
|
257
|
+
_get_sb().click_if_visible(selector, timeout=timeout)
|
|
258
|
+
return f"click_if_visible ran for {selector}"
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@mcp.tool()
|
|
262
|
+
def click_visible_elements(selector: str, limit: int = 0) -> str:
|
|
263
|
+
"""Click every currently-visible element matching a selector, in order
|
|
264
|
+
(e.g. checking every checkbox on a page). limit=0 means no limit."""
|
|
265
|
+
_get_sb().click_visible_elements(selector, limit=limit)
|
|
266
|
+
return f"Clicked visible elements matching {selector}"
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@mcp.tool()
|
|
270
|
+
def click_nth_element(selector: str, number: int) -> str:
|
|
271
|
+
"""Click the Nth element (1-indexed) matching a selector."""
|
|
272
|
+
_get_sb().click_nth_element(selector, number)
|
|
273
|
+
return f"Clicked element #{number} matching {selector}"
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@mcp.tool()
|
|
277
|
+
def click_link(link_text: str) -> str:
|
|
278
|
+
"""Click a link (<a> tag) by its visible text."""
|
|
279
|
+
_get_sb().click_link(link_text)
|
|
280
|
+
return f"Clicked link with text '{link_text}'"
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@mcp.tool()
|
|
284
|
+
def type_text(selector: str, text: str, timeout: int | None = None) -> str:
|
|
285
|
+
"""Clear a field and type text into it."""
|
|
286
|
+
_get_sb().type(selector, text, timeout=timeout)
|
|
287
|
+
return f"Typed into {selector}"
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@mcp.tool()
|
|
291
|
+
def send_keys(selector: str, text: str, timeout: int | None = None) -> str:
|
|
292
|
+
"""Send keystrokes to an element without clearing it first."""
|
|
293
|
+
_get_sb().send_keys(selector, text, timeout=timeout)
|
|
294
|
+
return f"Sent keys to {selector}"
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@mcp.tool()
|
|
298
|
+
def set_value(selector: str, text: str, timeout: int | None = None) -> str:
|
|
299
|
+
"""Set an input's value directly (e.g. for sliders, fast form fills)."""
|
|
300
|
+
_get_sb().set_value(selector, text, timeout=timeout)
|
|
301
|
+
return f"Set value of {selector}"
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@mcp.tool()
|
|
305
|
+
def clear_input(selector: str, timeout: int | None = None) -> str:
|
|
306
|
+
"""Clear an input field."""
|
|
307
|
+
_get_sb().clear_input(selector, timeout=timeout)
|
|
308
|
+
return f"Cleared {selector}"
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@mcp.tool()
|
|
312
|
+
def submit(selector: str) -> str:
|
|
313
|
+
"""Submit a form via a selector inside it."""
|
|
314
|
+
_get_sb().submit(selector)
|
|
315
|
+
return f"Submitted form via {selector}"
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@mcp.tool()
|
|
319
|
+
def select_option_by_text(dropdown_selector: str, option_text: str) -> str:
|
|
320
|
+
"""Select a <select> dropdown option by its visible text."""
|
|
321
|
+
_get_sb().select_option_by_text(dropdown_selector, option_text)
|
|
322
|
+
return f"Selected '{option_text}' in {dropdown_selector}"
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
@mcp.tool()
|
|
326
|
+
def select_option_by_value(dropdown_selector: str, value: str) -> str:
|
|
327
|
+
"""Select a <select> dropdown option by its value attribute."""
|
|
328
|
+
_get_sb().select_option_by_value(dropdown_selector, value)
|
|
329
|
+
return f"Selected value '{value}' in {dropdown_selector}"
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
@mcp.tool()
|
|
333
|
+
def select_option_by_index(dropdown_selector: str, index: int) -> str:
|
|
334
|
+
"""Select a <select> dropdown option by its 0-based index."""
|
|
335
|
+
_get_sb().select_option_by_index(dropdown_selector, index)
|
|
336
|
+
return f"Selected index {index} in {dropdown_selector}"
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@mcp.tool()
|
|
340
|
+
def focus(selector: str) -> str:
|
|
341
|
+
"""Move focus to an element."""
|
|
342
|
+
el = _get_sb().find_element(selector)
|
|
343
|
+
el.focus()
|
|
344
|
+
return f"Focused {selector}"
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@mcp.tool()
|
|
348
|
+
def highlight(selector: str) -> str:
|
|
349
|
+
"""Briefly highlight an element (useful when narrating actions on
|
|
350
|
+
a visible/headed browser)."""
|
|
351
|
+
_get_sb().highlight(selector)
|
|
352
|
+
return f"Highlighted {selector}"
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
@mcp.tool()
|
|
356
|
+
def nested_click(parent_selector: str, selector: str) -> str:
|
|
357
|
+
"""Click an element nested inside another (e.g. inside an iframe)."""
|
|
358
|
+
_get_sb().nested_click(parent_selector, selector)
|
|
359
|
+
return f"Clicked {selector} inside {parent_selector}"
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
# ---------------------------------------------------------------------------
|
|
363
|
+
# Waiting
|
|
364
|
+
# ---------------------------------------------------------------------------
|
|
365
|
+
|
|
366
|
+
@mcp.tool()
|
|
367
|
+
def wait_for_element(selector: str, timeout: int | None = None) -> str:
|
|
368
|
+
"""Wait until an element is present in the DOM."""
|
|
369
|
+
_get_sb().wait_for_element(selector, timeout=timeout)
|
|
370
|
+
return f"Element {selector} is present."
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@mcp.tool()
|
|
374
|
+
def wait_for_element_visible(selector: str, timeout: int | None = None) -> str:
|
|
375
|
+
"""Wait until an element is visible."""
|
|
376
|
+
_get_sb().wait_for_element_visible(selector, timeout=timeout)
|
|
377
|
+
return f"Element {selector} is visible."
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
@mcp.tool()
|
|
381
|
+
def wait_for_element_not_visible(
|
|
382
|
+
selector: str, timeout: int | None = None
|
|
383
|
+
) -> str:
|
|
384
|
+
"""Wait until an element is no longer visible."""
|
|
385
|
+
_get_sb().wait_for_element_not_visible(selector, timeout=timeout)
|
|
386
|
+
return f"Element {selector} is no longer visible."
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
@mcp.tool()
|
|
390
|
+
def wait_for_element_absent(selector: str, timeout: int | None = None) -> str:
|
|
391
|
+
"""Wait until an element is removed from the DOM."""
|
|
392
|
+
_get_sb().wait_for_element_absent(selector, timeout=timeout)
|
|
393
|
+
return f"Element {selector} is now absent."
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
@mcp.tool()
|
|
397
|
+
def wait_for_text(
|
|
398
|
+
text: str, selector: str = "body", timeout: int | None = None
|
|
399
|
+
) -> str:
|
|
400
|
+
"""Wait until specific text appears within an element."""
|
|
401
|
+
_get_sb().wait_for_text(text, selector, timeout=timeout)
|
|
402
|
+
return f"Text '{text}' appeared in {selector}."
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
# ---------------------------------------------------------------------------
|
|
406
|
+
# Assertions (raise an error, surfaced to the MCP client, if they fail)
|
|
407
|
+
# ---------------------------------------------------------------------------
|
|
408
|
+
|
|
409
|
+
@mcp.tool()
|
|
410
|
+
def assert_element(selector: str, timeout: int | None = None) -> str:
|
|
411
|
+
"""Assert an element is present in the DOM."""
|
|
412
|
+
_get_sb().assert_element(selector, timeout=timeout)
|
|
413
|
+
return f"Confirmed {selector} is present."
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
@mcp.tool()
|
|
417
|
+
def assert_element_visible(selector: str, timeout: int | None = None) -> str:
|
|
418
|
+
"""Assert an element is visible."""
|
|
419
|
+
_get_sb().assert_element_visible(selector, timeout=timeout)
|
|
420
|
+
return f"Confirmed {selector} is visible."
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
@mcp.tool()
|
|
424
|
+
def assert_text(
|
|
425
|
+
text: str, selector: str = "html", timeout: int | None = None
|
|
426
|
+
) -> str:
|
|
427
|
+
"""Assert text is present within an element."""
|
|
428
|
+
_get_sb().assert_text(text, selector, timeout=timeout)
|
|
429
|
+
return f"Confirmed '{text}' is present in {selector}."
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
@mcp.tool()
|
|
433
|
+
def assert_exact_text(
|
|
434
|
+
text: str, selector: str = "html", timeout: int | None = None
|
|
435
|
+
) -> str:
|
|
436
|
+
"""Assert an element's text matches exactly."""
|
|
437
|
+
_get_sb().assert_exact_text(text, selector, timeout=timeout)
|
|
438
|
+
return f"Confirmed {selector} text is exactly '{text}'."
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
@mcp.tool()
|
|
442
|
+
def assert_title(title: str) -> str:
|
|
443
|
+
"""Assert the page title matches exactly."""
|
|
444
|
+
_get_sb().assert_title(title)
|
|
445
|
+
return f"Confirmed title is '{title}'."
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
@mcp.tool()
|
|
449
|
+
def assert_url(url: str) -> str:
|
|
450
|
+
"""Assert the current URL matches exactly."""
|
|
451
|
+
_get_sb().assert_url(url)
|
|
452
|
+
return f"Confirmed URL is '{url}'."
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
@mcp.tool()
|
|
456
|
+
def assert_url_contains(substring: str) -> str:
|
|
457
|
+
"""Assert the current URL contains a substring."""
|
|
458
|
+
_get_sb().assert_url_contains(substring)
|
|
459
|
+
return f"Confirmed URL contains '{substring}'."
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
# ---------------------------------------------------------------------------
|
|
463
|
+
# Cookies & storage
|
|
464
|
+
# ---------------------------------------------------------------------------
|
|
465
|
+
|
|
466
|
+
@mcp.tool()
|
|
467
|
+
def get_all_cookies() -> Any:
|
|
468
|
+
"""Get all cookies for the current session."""
|
|
469
|
+
return _get_sb().get_all_cookies()
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
@mcp.tool()
|
|
473
|
+
def clear_cookies() -> str:
|
|
474
|
+
"""Clear all cookies."""
|
|
475
|
+
_get_sb().clear_cookies()
|
|
476
|
+
return "Cookies cleared."
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
@mcp.tool()
|
|
480
|
+
def save_cookies(name: str = "cookies.txt") -> str:
|
|
481
|
+
"""Save current cookies to a file."""
|
|
482
|
+
_get_sb().save_cookies(name=name)
|
|
483
|
+
return f"Cookies saved to {name}"
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
@mcp.tool()
|
|
487
|
+
def load_cookies(name: str = "cookies.txt") -> str:
|
|
488
|
+
"""Load cookies from a previously saved file."""
|
|
489
|
+
_get_sb().load_cookies(name=name)
|
|
490
|
+
return f"Cookies loaded from {name}"
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
@mcp.tool()
|
|
494
|
+
def get_local_storage_item(key: str) -> Any:
|
|
495
|
+
"""Get a value from the page's localStorage."""
|
|
496
|
+
return _get_sb().get_local_storage_item(key)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
@mcp.tool()
|
|
500
|
+
def set_local_storage_item(key: str, value: str) -> str:
|
|
501
|
+
"""Set a value in the page's localStorage."""
|
|
502
|
+
_get_sb().set_local_storage_item(key, value)
|
|
503
|
+
return f"Set localStorage[{key!r}]"
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
@mcp.tool()
|
|
507
|
+
def get_session_storage_item(key: str) -> Any:
|
|
508
|
+
"""Get a value from the page's sessionStorage."""
|
|
509
|
+
return _get_sb().get_session_storage_item(key)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
@mcp.tool()
|
|
513
|
+
def set_session_storage_item(key: str, value: str) -> str:
|
|
514
|
+
"""Set a value in the page's sessionStorage."""
|
|
515
|
+
_get_sb().set_session_storage_item(key, value)
|
|
516
|
+
return f"Set sessionStorage[{key!r}]"
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
# ---------------------------------------------------------------------------
|
|
520
|
+
# Scrolling
|
|
521
|
+
# ---------------------------------------------------------------------------
|
|
522
|
+
|
|
523
|
+
@mcp.tool()
|
|
524
|
+
def scroll_into_view(selector: str) -> str:
|
|
525
|
+
"""Scroll an element into view."""
|
|
526
|
+
_get_sb().scroll_into_view(selector)
|
|
527
|
+
return f"Scrolled {selector} into view."
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
@mcp.tool()
|
|
531
|
+
def scroll_to_top() -> str:
|
|
532
|
+
"""Scroll to the top of the page."""
|
|
533
|
+
_get_sb().scroll_to_top()
|
|
534
|
+
return "Scrolled to top."
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
@mcp.tool()
|
|
538
|
+
def scroll_to_bottom() -> str:
|
|
539
|
+
"""Scroll to the bottom of the page."""
|
|
540
|
+
_get_sb().scroll_to_bottom()
|
|
541
|
+
return "Scrolled to bottom."
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
@mcp.tool()
|
|
545
|
+
def scroll_up(amount: int = 25) -> str:
|
|
546
|
+
"""Scroll up by a relative amount."""
|
|
547
|
+
_get_sb().scroll_up(amount=amount)
|
|
548
|
+
return f"Scrolled up {amount}."
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
@mcp.tool()
|
|
552
|
+
def scroll_down(amount: int = 25) -> str:
|
|
553
|
+
"""Scroll down by a relative amount."""
|
|
554
|
+
_get_sb().scroll_down(amount=amount)
|
|
555
|
+
return f"Scrolled down {amount}."
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
# ---------------------------------------------------------------------------
|
|
559
|
+
# Windows & tabs
|
|
560
|
+
# ---------------------------------------------------------------------------
|
|
561
|
+
|
|
562
|
+
@mcp.tool()
|
|
563
|
+
def get_window_rect() -> dict:
|
|
564
|
+
"""Get the current window's position and size."""
|
|
565
|
+
return _get_sb().get_window_rect()
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
@mcp.tool()
|
|
569
|
+
def set_window_rect(x: int, y: int, width: int, height: int) -> str:
|
|
570
|
+
"""Set the current window's position and size."""
|
|
571
|
+
_get_sb().set_window_rect(x, y, width, height)
|
|
572
|
+
return f"Window set to ({x}, {y}, {width}x{height})"
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
@mcp.tool()
|
|
576
|
+
def maximize() -> str:
|
|
577
|
+
"""Maximize the browser window."""
|
|
578
|
+
_get_sb().maximize()
|
|
579
|
+
return "Window maximized."
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
@mcp.tool()
|
|
583
|
+
def minimize() -> str:
|
|
584
|
+
"""Minimize the browser window."""
|
|
585
|
+
_get_sb().minimize()
|
|
586
|
+
return "Window minimized."
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
@mcp.tool()
|
|
590
|
+
def open_new_tab(url: str | None = None, switch_to: bool = True) -> str:
|
|
591
|
+
"""Open a new browser tab, optionally navigating and switching to it."""
|
|
592
|
+
_get_sb().open_new_tab(url=url, switch_to=switch_to)
|
|
593
|
+
return f"Opened new tab (url={url!r}, switch_to={switch_to})"
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
@mcp.tool()
|
|
597
|
+
def switch_to_tab(tab_index: int) -> str:
|
|
598
|
+
"""Switch to a tab by its index (as returned by get_tabs)."""
|
|
599
|
+
tabs = _get_sb().get_tabs()
|
|
600
|
+
_get_sb().switch_to_tab(tabs[tab_index])
|
|
601
|
+
return f"Switched to tab {tab_index}"
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
@mcp.tool()
|
|
605
|
+
def switch_to_newest_tab() -> str:
|
|
606
|
+
"""Switch to the most recently opened tab."""
|
|
607
|
+
_get_sb().switch_to_newest_tab()
|
|
608
|
+
return "Switched to newest tab."
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
@mcp.tool()
|
|
612
|
+
def close_active_tab() -> str:
|
|
613
|
+
"""Close the currently active tab."""
|
|
614
|
+
_get_sb().close_active_tab()
|
|
615
|
+
return "Closed active tab."
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
@mcp.tool()
|
|
619
|
+
def get_tabs_count() -> int:
|
|
620
|
+
"""Get how many tabs are currently open."""
|
|
621
|
+
return len(_get_sb().get_tabs())
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
# ---------------------------------------------------------------------------
|
|
625
|
+
# Captcha solving
|
|
626
|
+
# ---------------------------------------------------------------------------
|
|
627
|
+
|
|
628
|
+
@mcp.tool()
|
|
629
|
+
def solve_captcha() -> str:
|
|
630
|
+
"""Attempt to solve a captcha (e.g. Cloudflare Turnstile) on the page."""
|
|
631
|
+
_get_sb().solve_captcha()
|
|
632
|
+
return "Attempted captcha solve."
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
# ---------------------------------------------------------------------------
|
|
636
|
+
# Output & misc
|
|
637
|
+
# ---------------------------------------------------------------------------
|
|
638
|
+
|
|
639
|
+
@mcp.tool()
|
|
640
|
+
def save_screenshot(
|
|
641
|
+
name: str = "screenshot.png", folder: str | None = None
|
|
642
|
+
) -> str:
|
|
643
|
+
"""Save a screenshot of the current page."""
|
|
644
|
+
_get_sb().save_screenshot(name, folder=folder)
|
|
645
|
+
return f"Screenshot saved as {name}"
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
@mcp.tool()
|
|
649
|
+
def save_page_source(
|
|
650
|
+
name: str = "page_source.html", folder: str | None = None
|
|
651
|
+
) -> str:
|
|
652
|
+
"""Save the current page's HTML source to a file."""
|
|
653
|
+
_get_sb().save_page_source(name, folder=folder)
|
|
654
|
+
return f"Page source saved as {name}"
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
@mcp.tool()
|
|
658
|
+
def save_as_pdf(name: str = "page.pdf", folder: str | None = None) -> str:
|
|
659
|
+
"""Print the current page to a PDF file."""
|
|
660
|
+
_get_sb().save_as_pdf(name, folder=folder)
|
|
661
|
+
return f"Page saved as PDF: {name}"
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
@mcp.tool()
|
|
665
|
+
def evaluate(expression: str) -> Any:
|
|
666
|
+
"""Evaluate a JavaScript expression in the page context and return the
|
|
667
|
+
result. Equivalent to execute_script."""
|
|
668
|
+
return _get_sb().evaluate(expression)
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
@mcp.tool()
|
|
672
|
+
def sleep(seconds: float) -> str:
|
|
673
|
+
"""Pause execution for a number of seconds."""
|
|
674
|
+
_get_sb().sleep(seconds)
|
|
675
|
+
return f"Slept {seconds}s"
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
@mcp.tool()
|
|
679
|
+
def get_user_agent() -> str:
|
|
680
|
+
"""Get the browser's current user agent string."""
|
|
681
|
+
return _get_sb().get_user_agent()
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def main():
|
|
685
|
+
mcp.run(transport="stdio")
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
if __name__ == "__main__":
|
|
689
|
+
main()
|