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.
@@ -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()