This is page 4 of 4. Use http://codebase.md/vibheksoni/stealth-browser-mcp?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .dockerignore ├── .github │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.md │ │ ├── config.yml │ │ ├── feature_request.md │ │ └── showcase.yml │ ├── labeler.yml │ ├── pull_request_template.md │ └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Checklist.md ├── CODE_OF_CONDUCT.md ├── CODEOWNERS ├── COMPARISON.md ├── CONTRIBUTING.md ├── demo │ ├── augment-hero-clone.md │ ├── augment-hero-recreation.html │ └── README.md ├── Dockerfile ├── examples │ └── claude_prompts.md ├── HALL_OF_FAME.md ├── LICENSE ├── media │ ├── AugmentHeroClone.PNG │ ├── Showcase Stealth Browser Mcp.mp4 │ ├── showcase-demo-full.gif │ ├── showcase-demo.gif │ └── UndetectedStealthBrowser.png ├── pyproject.toml ├── README.md ├── requirements.txt ├── ROADMAP.md ├── run_server.bat ├── run_server.sh ├── SECURITY.md ├── smithery.yaml └── src ├── __init__.py ├── browser_manager.py ├── cdp_element_cloner.py ├── cdp_function_executor.py ├── comprehensive_element_cloner.py ├── debug_logger.py ├── dom_handler.py ├── dynamic_hook_ai_interface.py ├── dynamic_hook_system.py ├── element_cloner.py ├── file_based_element_cloner.py ├── hook_learning_system.py ├── js │ ├── comprehensive_element_extractor.js │ ├── extract_animations.js │ ├── extract_assets.js │ ├── extract_events.js │ ├── extract_related_files.js │ ├── extract_structure.js │ └── extract_styles.js ├── models.py ├── network_interceptor.py ├── persistent_storage.py ├── platform_utils.py ├── process_cleanup.py ├── progressive_element_cloner.py ├── response_handler.py ├── response_stage_hooks.py └── server.py ``` # Files -------------------------------------------------------------------------------- /src/server.py: -------------------------------------------------------------------------------- ```python 1 | """Main MCP server for browser automation.""" 2 | 3 | import asyncio 4 | import base64 5 | import importlib 6 | import json 7 | import os 8 | import signal 9 | import sys 10 | import tempfile 11 | from contextlib import asynccontextmanager 12 | from datetime import datetime 13 | from pathlib import Path 14 | from typing import Any, Dict, List, Optional, Union 15 | 16 | import nodriver as uc 17 | from fastmcp import FastMCP 18 | 19 | from browser_manager import BrowserManager 20 | from cdp_element_cloner import CDPElementCloner 21 | from cdp_function_executor import CDPFunctionExecutor 22 | from comprehensive_element_cloner import comprehensive_element_cloner 23 | from debug_logger import debug_logger 24 | from dom_handler import DOMHandler 25 | from element_cloner import element_cloner 26 | from file_based_element_cloner import file_based_element_cloner 27 | from models import ( 28 | BrowserOptions, 29 | NavigationOptions, 30 | ScriptResult, 31 | BrowserState, 32 | PageState, 33 | ) 34 | from network_interceptor import NetworkInterceptor 35 | from dynamic_hook_system import dynamic_hook_system 36 | from dynamic_hook_ai_interface import dynamic_hook_ai 37 | from persistent_storage import persistent_storage 38 | from progressive_element_cloner import progressive_element_cloner 39 | from response_handler import response_handler 40 | from platform_utils import validate_browser_environment, get_platform_info 41 | from process_cleanup import process_cleanup 42 | 43 | DISABLED_SECTIONS = set() 44 | 45 | def is_section_enabled(section: str) -> bool: 46 | """Check if a tool section is enabled.""" 47 | return section not in DISABLED_SECTIONS 48 | 49 | def section_tool(section: str): 50 | """Decorator to conditionally register tools based on section status.""" 51 | def decorator(func): 52 | if is_section_enabled(section): 53 | return mcp.tool(func) 54 | else: 55 | return func 56 | return decorator 57 | 58 | @asynccontextmanager 59 | async def app_lifespan(server): 60 | """ 61 | Manage application lifecycle with proper cleanup. 62 | 63 | Args: 64 | server (Any): The server instance for which the lifespan is being managed. 65 | """ 66 | debug_logger.log_info("server", "startup", "Starting Browser Automation MCP Server...") 67 | try: 68 | yield 69 | finally: 70 | debug_logger.log_info("server", "shutdown", "Shutting down Browser Automation MCP Server...") 71 | try: 72 | await browser_manager.close_all() 73 | debug_logger.log_info("server", "cleanup", "All browser instances closed") 74 | except Exception as e: 75 | debug_logger.log_error("server", "cleanup", e) 76 | 77 | try: 78 | process_cleanup._cleanup_all_tracked() 79 | debug_logger.log_info("server", "cleanup", "Process cleanup complete") 80 | except Exception as e: 81 | debug_logger.log_error("server", "cleanup", f"Process cleanup failed: {e}") 82 | try: 83 | persistent_instances = persistent_storage.list_instances() 84 | if persistent_instances.get("instances"): 85 | debug_logger.log_info( 86 | "server", 87 | "storage_cleanup", 88 | f"Clearing in-memory storage with {len(persistent_instances['instances'])} instances...", 89 | ) 90 | persistent_storage.clear_all() 91 | debug_logger.log_info("server", "storage_cleanup", "In-memory storage cleared") 92 | except Exception as e: 93 | debug_logger.log_error("server", "storage_cleanup", e) 94 | debug_logger.log_info("server", "shutdown", "Browser Automation MCP Server shutdown complete") 95 | 96 | mcp = FastMCP( 97 | name="Browser Automation MCP", 98 | instructions=""" 99 | This MCP server provides undetectable browser automation using nodriver (CDP-based). 100 | 101 | Key features: 102 | - Spawn and manage multiple browser instances 103 | - Navigate and interact with web pages 104 | - Query and manipulate DOM elements 105 | - Intercept and analyze network traffic 106 | - Execute JavaScript in page context 107 | - Manage cookies and storage 108 | 109 | All browser instances are undetectable by anti-bot systems. 110 | """, 111 | lifespan=app_lifespan, 112 | ) 113 | 114 | browser_manager = BrowserManager() 115 | network_interceptor = NetworkInterceptor() 116 | dom_handler = DOMHandler() 117 | cdp_function_executor = CDPFunctionExecutor() 118 | 119 | @section_tool("browser-management") 120 | async def spawn_browser( 121 | headless: bool = False, 122 | user_agent: Optional[str] = None, 123 | viewport_width: int = 1920, 124 | viewport_height: int = 1080, 125 | proxy: Optional[str] = None, 126 | block_resources: List[str] = None, 127 | extra_headers: Dict[str, str] = None, 128 | user_data_dir: Optional[str] = None, 129 | sandbox: Optional[Any] = None 130 | ) -> Dict[str, Any]: 131 | """ 132 | Spawn a new browser instance. 133 | 134 | Args: 135 | headless (bool): Run in headless mode. 136 | user_agent (Optional[str]): Custom user agent string. 137 | viewport_width (int): Viewport width in pixels. 138 | viewport_height (int): Viewport height in pixels. 139 | proxy (Optional[str]): Proxy server URL. 140 | block_resources (List[str]): List of resource types to block (e.g., ['image', 'font', 'stylesheet']). 141 | extra_headers (Dict[str, str]): Additional HTTP headers. 142 | user_data_dir (Optional[str]): Path to user data directory for persistent sessions. 143 | sandbox (Optional[Any]): Enable browser sandbox. Accepts bool, string ('true'/'false'), int (1/0), or None for auto-detect. 144 | 145 | Returns: 146 | Dict[str, Any]: Instance information including instance_id. 147 | """ 148 | try: 149 | from platform_utils import is_running_as_root, is_running_in_container 150 | 151 | if sandbox is None: 152 | sandbox = not (is_running_as_root() or is_running_in_container()) 153 | elif isinstance(sandbox, str): 154 | sandbox = sandbox.lower() in ('true', '1', 'yes', 'on', 'enabled') 155 | elif isinstance(sandbox, int): 156 | sandbox = bool(sandbox) 157 | elif not isinstance(sandbox, bool): 158 | sandbox = bool(sandbox) 159 | 160 | options = BrowserOptions( 161 | headless=headless, 162 | user_agent=user_agent, 163 | viewport_width=viewport_width, 164 | viewport_height=viewport_height, 165 | proxy=proxy, 166 | block_resources=block_resources or [], 167 | extra_headers=extra_headers or {}, 168 | user_data_dir=user_data_dir, 169 | sandbox=sandbox 170 | ) 171 | instance = await browser_manager.spawn_browser(options) 172 | tab = await browser_manager.get_tab(instance.instance_id) 173 | if tab: 174 | await network_interceptor.setup_interception( 175 | tab, instance.instance_id, block_resources 176 | ) 177 | await dynamic_hook_system.setup_interception(tab, instance.instance_id) 178 | dynamic_hook_system.add_instance(instance.instance_id) 179 | return { 180 | "instance_id": instance.instance_id, 181 | "state": instance.state, 182 | "headless": instance.headless, 183 | "viewport": instance.viewport 184 | } 185 | except Exception as e: 186 | raise Exception(f"Failed to spawn browser: {str(e)}") 187 | 188 | @section_tool("browser-management") 189 | async def list_instances() -> List[Dict[str, Any]]: 190 | """ 191 | List all active browser instances. 192 | 193 | Returns: 194 | List[Dict[str, Any]]: List of browser instances with their current state. 195 | """ 196 | memory_instances = await browser_manager.list_instances() 197 | storage_instances = persistent_storage.list_instances() 198 | result = [] 199 | for inst in memory_instances: 200 | result.append({ 201 | "instance_id": inst.instance_id, 202 | "state": inst.state, 203 | "current_url": inst.current_url, 204 | "title": inst.title, 205 | "source": "active" 206 | }) 207 | memory_ids = {inst.instance_id for inst in memory_instances} 208 | for instance_id, inst_data in storage_instances.get("instances", {}).items(): 209 | if instance_id not in memory_ids: 210 | result.append({ 211 | "instance_id": inst_data["instance_id"], 212 | "state": inst_data["state"] + " (stored)", 213 | "current_url": inst_data["current_url"], 214 | "title": inst_data["title"], 215 | "source": "stored" 216 | }) 217 | return result 218 | 219 | @section_tool("browser-management") 220 | async def close_instance(instance_id: str) -> bool: 221 | """ 222 | Close a browser instance. 223 | 224 | Args: 225 | instance_id (str): Browser instance ID. 226 | 227 | Returns: 228 | bool: True if closed successfully. 229 | """ 230 | success = await browser_manager.close_instance(instance_id) 231 | if success: 232 | await network_interceptor.clear_instance_data(instance_id) 233 | return success 234 | 235 | @section_tool("browser-management") 236 | async def get_instance_state(instance_id: str) -> Optional[Dict[str, Any]]: 237 | """ 238 | Get detailed state of a browser instance. 239 | 240 | Args: 241 | instance_id (str): Browser instance ID. 242 | 243 | Returns: 244 | Optional[Dict[str, Any]]: Complete state information. 245 | """ 246 | state = await browser_manager.get_page_state(instance_id) 247 | if state: 248 | return state.dict() 249 | return None 250 | 251 | @section_tool("browser-management") 252 | async def navigate( 253 | instance_id: str, 254 | url: str, 255 | wait_until: str = "load", 256 | timeout: int = 30000, 257 | referrer: Optional[str] = None 258 | ) -> Dict[str, Any]: 259 | """ 260 | Navigate to a URL. 261 | 262 | Args: 263 | instance_id (str): Browser instance ID. 264 | url (str): URL to navigate to. 265 | wait_until (str): Wait condition - 'load', 'domcontentloaded', or 'networkidle'. 266 | timeout (int): Navigation timeout in milliseconds. 267 | referrer (Optional[str]): Referrer URL. 268 | 269 | Returns: 270 | Dict[str, Any]: Navigation result with final URL and title. 271 | """ 272 | if isinstance(timeout, str): 273 | timeout = int(timeout) 274 | tab = await browser_manager.get_tab(instance_id) 275 | if not tab: 276 | raise Exception(f"Instance not found: {instance_id}") 277 | try: 278 | if referrer: 279 | await tab.send(uc.cdp.page.set_referrer_policy( 280 | referrerPolicy='origin-when-cross-origin' 281 | )) 282 | await tab.get(url) 283 | if wait_until == "domcontentloaded": 284 | await tab.wait(uc.cdp.page.DomContentEventFired) 285 | elif wait_until == "networkidle": 286 | await asyncio.sleep(2) 287 | else: 288 | await tab.wait(uc.cdp.page.LoadEventFired) 289 | final_url = await tab.evaluate("window.location.href") 290 | title = await tab.evaluate("document.title") 291 | await browser_manager.update_instance_state(instance_id, final_url, title) 292 | return { 293 | "url": final_url, 294 | "title": title, 295 | "success": True 296 | } 297 | except Exception as e: 298 | raise 299 | 300 | @section_tool("browser-management") 301 | async def go_back(instance_id: str) -> bool: 302 | """ 303 | Navigate back in history. 304 | 305 | Args: 306 | instance_id (str): Browser instance ID. 307 | 308 | Returns: 309 | bool: True if navigation was successful. 310 | """ 311 | tab = await browser_manager.get_tab(instance_id) 312 | if not tab: 313 | raise Exception(f"Instance not found: {instance_id}") 314 | await tab.back() 315 | return True 316 | 317 | @section_tool("browser-management") 318 | async def go_forward(instance_id: str) -> bool: 319 | """ 320 | Navigate forward in history. 321 | 322 | Args: 323 | instance_id (str): Browser instance ID. 324 | 325 | Returns: 326 | bool: True if navigation was successful. 327 | """ 328 | tab = await browser_manager.get_tab(instance_id) 329 | if not tab: 330 | raise Exception(f"Instance not found: {instance_id}") 331 | await tab.forward() 332 | return True 333 | 334 | @section_tool("browser-management") 335 | async def reload_page(instance_id: str, ignore_cache: bool = False) -> bool: 336 | """ 337 | Reload the current page. 338 | 339 | Args: 340 | instance_id (str): Browser instance ID. 341 | ignore_cache (bool): Whether to ignore cache when reloading. 342 | 343 | Returns: 344 | bool: True if reload was successful. 345 | """ 346 | tab = await browser_manager.get_tab(instance_id) 347 | if not tab: 348 | raise Exception(f"Instance not found: {instance_id}") 349 | await tab.reload() 350 | return True 351 | 352 | @section_tool("element-interaction") 353 | async def query_elements( 354 | instance_id: str, 355 | selector: str, 356 | text_filter: Optional[str] = None, 357 | visible_only: bool = True, 358 | limit: Optional[Any] = None 359 | ) -> List[Dict[str, Any]]: 360 | """ 361 | Query DOM elements. 362 | 363 | Args: 364 | instance_id (str): Browser instance ID. 365 | selector (str): CSS selector or XPath (starts with '//'). 366 | text_filter (Optional[str]): Filter by text content. 367 | visible_only (bool): Only return visible elements. 368 | limit (Optional[Any]): Maximum number of elements to return. 369 | 370 | Returns: 371 | List[Dict[str, Any]]: List of matching elements with their properties. 372 | """ 373 | tab = await browser_manager.get_tab(instance_id) 374 | if not tab: 375 | raise Exception(f"Instance not found: {instance_id}") 376 | debug_logger.log_info('Server', 'query_elements', f'Received limit parameter: {limit} (type: {type(limit)})') 377 | elements = await dom_handler.query_elements( 378 | tab, selector, text_filter, visible_only, limit 379 | ) 380 | debug_logger.log_info('Server', 'query_elements', f'DOM handler returned {len(elements)} elements') 381 | result = [] 382 | for i, elem in enumerate(elements): 383 | try: 384 | if hasattr(elem, 'model_dump'): 385 | elem_dict = elem.model_dump() 386 | else: 387 | elem_dict = elem.dict() 388 | result.append(elem_dict) 389 | debug_logger.log_info('Server', 'query_elements', f'Converted element {i+1} to dict: {list(elem_dict.keys())}') 390 | except Exception as e: 391 | debug_logger.log_error('Server', 'query_elements', e, {'element_index': i}) 392 | debug_logger.log_info('Server', 'query_elements', f'Returning {len(result)} results to MCP client') 393 | return result if result else [] 394 | 395 | @section_tool("element-interaction") 396 | async def click_element( 397 | instance_id: str, 398 | selector: str, 399 | text_match: Optional[str] = None, 400 | timeout: int = 10000 401 | ) -> bool: 402 | """ 403 | Click an element. 404 | 405 | Args: 406 | instance_id (str): Browser instance ID. 407 | selector (str): CSS selector or XPath. 408 | text_match (Optional[str]): Click element with matching text. 409 | timeout (int): Timeout in milliseconds. 410 | 411 | Returns: 412 | bool: True if clicked successfully. 413 | """ 414 | if isinstance(timeout, str): 415 | timeout = int(timeout) 416 | tab = await browser_manager.get_tab(instance_id) 417 | if not tab: 418 | raise Exception(f"Instance not found: {instance_id}") 419 | return await dom_handler.click_element(tab, selector, text_match, timeout) 420 | 421 | @section_tool("element-interaction") 422 | async def type_text( 423 | instance_id: str, 424 | selector: str, 425 | text: str, 426 | clear_first: bool = True, 427 | delay_ms: int = 50, 428 | parse_newlines: bool = False, 429 | shift_enter: bool = False 430 | ) -> bool: 431 | """ 432 | Type text into an input field. 433 | 434 | Args: 435 | instance_id (str): Browser instance ID. 436 | selector (str): CSS selector or XPath. 437 | text (str): Text to type. 438 | clear_first (bool): Clear field before typing. 439 | delay_ms (int): Delay between keystrokes in milliseconds. 440 | parse_newlines (bool): If True, parse \n as Enter key presses. 441 | shift_enter (bool): If True, use Shift+Enter instead of Enter (for chat apps). 442 | 443 | Returns: 444 | bool: True if typed successfully. 445 | """ 446 | if isinstance(delay_ms, str): 447 | delay_ms = int(delay_ms) 448 | tab = await browser_manager.get_tab(instance_id) 449 | if not tab: 450 | raise Exception(f"Instance not found: {instance_id}") 451 | return await dom_handler.type_text(tab, selector, text, clear_first, delay_ms, parse_newlines, shift_enter) 452 | 453 | @section_tool("element-interaction") 454 | async def paste_text( 455 | instance_id: str, 456 | selector: str, 457 | text: str, 458 | clear_first: bool = True 459 | ) -> bool: 460 | """ 461 | Paste text instantly into an input field. 462 | 463 | Args: 464 | instance_id (str): Browser instance ID. 465 | selector (str): CSS selector or XPath. 466 | text (str): Text to paste. 467 | clear_first (bool): Clear field before pasting. 468 | 469 | Returns: 470 | bool: True if pasted successfully. 471 | """ 472 | tab = await browser_manager.get_tab(instance_id) 473 | if not tab: 474 | raise Exception(f"Instance not found: {instance_id}") 475 | return await dom_handler.paste_text(tab, selector, text, clear_first) 476 | 477 | @section_tool("element-interaction") 478 | async def select_option( 479 | instance_id: str, 480 | selector: str, 481 | value: Optional[str] = None, 482 | text: Optional[str] = None, 483 | index: Optional[Any] = None 484 | ) -> bool: 485 | """ 486 | Select an option from a dropdown. 487 | 488 | Args: 489 | instance_id (str): Browser instance ID. 490 | selector (str): CSS selector for the select element. 491 | value (Optional[str]): Option value attribute. 492 | text (Optional[str]): Option text content. 493 | index (Optional[Any]): Option index (0-based). Can be string or int. 494 | 495 | Returns: 496 | bool: True if selected successfully. 497 | """ 498 | tab = await browser_manager.get_tab(instance_id) 499 | if not tab: 500 | raise Exception(f"Instance not found: {instance_id}") 501 | 502 | converted_index = None 503 | if index is not None: 504 | try: 505 | converted_index = int(index) 506 | except (ValueError, TypeError): 507 | raise Exception(f"Invalid index value: {index}. Must be a number.") 508 | 509 | return await dom_handler.select_option(tab, selector, value, text, converted_index) 510 | 511 | @section_tool("element-interaction") 512 | async def get_element_state( 513 | instance_id: str, 514 | selector: str 515 | ) -> Dict[str, Any]: 516 | """ 517 | Get complete state of an element. 518 | 519 | Args: 520 | instance_id (str): Browser instance ID. 521 | selector (str): CSS selector or XPath. 522 | 523 | Returns: 524 | Dict[str, Any]: Element state including attributes, style, position, etc. 525 | """ 526 | tab = await browser_manager.get_tab(instance_id) 527 | if not tab: 528 | raise Exception(f"Instance not found: {instance_id}") 529 | return await dom_handler.get_element_state(tab, selector) 530 | 531 | @section_tool("element-interaction") 532 | async def wait_for_element( 533 | instance_id: str, 534 | selector: str, 535 | timeout: int = 30000, 536 | visible: bool = True, 537 | text_content: Optional[str] = None 538 | ) -> bool: 539 | """ 540 | Wait for an element to appear. 541 | 542 | Args: 543 | instance_id (str): Browser instance ID. 544 | selector (str): CSS selector or XPath. 545 | timeout (int): Timeout in milliseconds. 546 | visible (bool): Wait for element to be visible. 547 | text_content (Optional[str]): Wait for specific text content. 548 | 549 | Returns: 550 | bool: True if element found. 551 | """ 552 | if isinstance(timeout, str): 553 | timeout = int(timeout) 554 | tab = await browser_manager.get_tab(instance_id) 555 | if not tab: 556 | raise Exception(f"Instance not found: {instance_id}") 557 | return await dom_handler.wait_for_element(tab, selector, timeout, visible, text_content) 558 | 559 | @section_tool("element-interaction") 560 | async def scroll_page( 561 | instance_id: str, 562 | direction: str = "down", 563 | amount: int = 500, 564 | smooth: bool = True 565 | ) -> bool: 566 | """ 567 | Scroll the page. 568 | 569 | Args: 570 | instance_id (str): Browser instance ID. 571 | direction (str): 'down', 'up', 'left', 'right', 'top', or 'bottom'. 572 | amount (int): Pixels to scroll (ignored for 'top' and 'bottom'). 573 | smooth (bool): Use smooth scrolling. 574 | 575 | Returns: 576 | bool: True if scrolled successfully. 577 | """ 578 | if isinstance(amount, str): 579 | amount = int(amount) 580 | tab = await browser_manager.get_tab(instance_id) 581 | if not tab: 582 | raise Exception(f"Instance not found: {instance_id}") 583 | return await dom_handler.scroll_page(tab, direction, amount, smooth) 584 | 585 | @section_tool("element-interaction") 586 | async def execute_script( 587 | instance_id: str, 588 | script: str, 589 | args: Optional[List[Any]] = None 590 | ) -> Dict[str, Any]: 591 | """ 592 | Execute JavaScript in page context. 593 | 594 | Args: 595 | instance_id (str): Browser instance ID. 596 | script (str): JavaScript code to execute. 597 | args (Optional[List[Any]]): Arguments to pass to the script. 598 | 599 | Returns: 600 | Dict[str, Any]: Script execution result. 601 | """ 602 | tab = await browser_manager.get_tab(instance_id) 603 | if not tab: 604 | raise Exception(f"Instance not found: {instance_id}") 605 | try: 606 | result = await dom_handler.execute_script(tab, script, args) 607 | return { 608 | "success": True, 609 | "result": result, 610 | "error": None 611 | } 612 | except Exception as e: 613 | return { 614 | "success": False, 615 | "result": None, 616 | "error": str(e) 617 | } 618 | 619 | @section_tool("element-interaction") 620 | async def get_page_content( 621 | instance_id: str, 622 | include_frames: bool = False 623 | ) -> Dict[str, Any]: 624 | """ 625 | Get page HTML and text content. 626 | 627 | Args: 628 | instance_id (str): Browser instance ID. 629 | include_frames (bool): Include iframe information. 630 | 631 | Returns: 632 | Dict[str, Any]: Page content including HTML, text, and metadata. 633 | """ 634 | tab = await browser_manager.get_tab(instance_id) 635 | if not tab: 636 | raise Exception(f"Instance not found: {instance_id}") 637 | content = await dom_handler.get_page_content(tab, include_frames) 638 | 639 | return response_handler.handle_response( 640 | content, 641 | "page_content", 642 | {"instance_id": instance_id, "include_frames": include_frames} 643 | ) 644 | 645 | @section_tool("element-interaction") 646 | async def take_screenshot( 647 | instance_id: str, 648 | full_page: bool = False, 649 | format: str = "png", 650 | file_path: Optional[str] = None 651 | ) -> Union[str, Dict[str, Any]]: 652 | """ 653 | Take a screenshot of the page. 654 | 655 | Args: 656 | instance_id (str): Browser instance ID. 657 | full_page (bool): Capture full page (not just viewport). 658 | format (str): Image format ('png' or 'jpeg'). 659 | file_path (Optional[str]): Optional file path to save screenshot to. 660 | 661 | Returns: 662 | Union[str, Dict]: File path if file_path provided, otherwise optimized base64 data or file info dict. 663 | """ 664 | from PIL import Image 665 | import io 666 | 667 | tab = await browser_manager.get_tab(instance_id) 668 | if not tab: 669 | raise Exception(f"Instance not found: {instance_id}") 670 | 671 | if file_path: 672 | save_path = Path(file_path) 673 | save_path.parent.mkdir(parents=True, exist_ok=True) 674 | await tab.save_screenshot(save_path) 675 | return f"Screenshot saved. AI agents should use the Read tool to view this image: {str(save_path.absolute())}" 676 | 677 | with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp_file: 678 | tmp_path = Path(tmp_file.name) 679 | 680 | try: 681 | await tab.save_screenshot(tmp_path) 682 | 683 | with Image.open(tmp_path) as img: 684 | if img.mode in ('RGBA', 'LA', 'P') and format.lower() == 'jpeg': 685 | background = Image.new('RGB', img.size, (255, 255, 255)) 686 | if img.mode == 'P': 687 | img = img.convert('RGBA') 688 | background.paste(img, mask=img.split()[-1] if img.mode in ('RGBA', 'LA') else None) 689 | img = background 690 | 691 | output_buffer = io.BytesIO() 692 | 693 | if format.lower() == 'jpeg': 694 | img.save(output_buffer, format='JPEG', quality=85, optimize=True) 695 | else: 696 | img.save(output_buffer, format='PNG', optimize=True) 697 | 698 | compressed_bytes = output_buffer.getvalue() 699 | 700 | base64_size = len(compressed_bytes) * 1.33 701 | estimated_tokens = int(base64_size / 4) 702 | 703 | if estimated_tokens > 20000: 704 | timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") 705 | screenshot_filename = f"screenshot_{timestamp}_{instance_id[:8]}.{format.lower()}" 706 | screenshot_path = response_handler.clone_dir / screenshot_filename 707 | 708 | with open(screenshot_path, 'wb') as f: 709 | f.write(compressed_bytes) 710 | 711 | file_size_kb = len(compressed_bytes) / 1024 712 | return { 713 | "file_path": str(screenshot_path), 714 | "filename": screenshot_filename, 715 | "file_size_kb": round(file_size_kb, 2), 716 | "estimated_tokens": estimated_tokens, 717 | "reason": "Screenshot too large, automatically saved to file", 718 | "message": f"Screenshot saved. AI agents should use the Read tool to view this image: {str(screenshot_path)}" 719 | } 720 | 721 | return base64.b64encode(compressed_bytes).decode('utf-8') 722 | 723 | finally: 724 | if tmp_path.exists(): 725 | os.unlink(tmp_path) 726 | 727 | 728 | @section_tool("network-debugging") 729 | async def list_network_requests( 730 | instance_id: str, 731 | filter_type: Optional[str] = None 732 | ) -> Union[List[Dict[str, Any]], Dict[str, Any]]: 733 | """ 734 | List captured network requests. 735 | 736 | Args: 737 | instance_id (str): Browser instance ID. 738 | filter_type (Optional[str]): Filter by resource type (e.g., 'image', 'script', 'xhr'). 739 | 740 | Returns: 741 | Union[List[Dict[str, Any]], Dict[str, Any]]: List of network requests, or file metadata if response too large. 742 | """ 743 | requests = await network_interceptor.list_requests(instance_id, filter_type) 744 | formatted_requests = [ 745 | { 746 | "request_id": req.request_id, 747 | "url": req.url, 748 | "method": req.method, 749 | "resource_type": req.resource_type, 750 | "timestamp": req.timestamp.isoformat() 751 | } 752 | for req in requests 753 | ] 754 | 755 | return response_handler.handle_response(formatted_requests, "network_requests") 756 | 757 | 758 | @section_tool("network-debugging") 759 | async def get_request_details( 760 | request_id: str 761 | ) -> Optional[Dict[str, Any]]: 762 | """ 763 | Get detailed information about a network request. 764 | 765 | Args: 766 | request_id (str): Network request ID. 767 | 768 | Returns: 769 | Optional[Dict[str, Any]]: Request details including headers, cookies, and body. 770 | """ 771 | request = await network_interceptor.get_request(request_id) 772 | if request: 773 | return request.dict() 774 | return None 775 | 776 | 777 | @section_tool("network-debugging") 778 | async def get_response_details( 779 | request_id: str 780 | ) -> Optional[Dict[str, Any]]: 781 | """ 782 | Get response details for a network request. 783 | 784 | Args: 785 | request_id (str): Network request ID. 786 | 787 | Returns: 788 | Optional[Dict[str, Any]]: Response details including status, headers, and metadata. 789 | """ 790 | response = await network_interceptor.get_response(request_id) 791 | if response: 792 | return response.dict() 793 | return None 794 | 795 | 796 | @section_tool("network-debugging") 797 | async def get_response_content( 798 | instance_id: str, 799 | request_id: str 800 | ) -> Optional[str]: 801 | """ 802 | Get response body content. 803 | 804 | Args: 805 | instance_id (str): Browser instance ID. 806 | request_id (str): Network request ID. 807 | 808 | Returns: 809 | Optional[str]: Response body as text (base64 encoded for binary). 810 | """ 811 | tab = await browser_manager.get_tab(instance_id) 812 | if not tab: 813 | raise Exception(f"Instance not found: {instance_id}") 814 | body = await network_interceptor.get_response_body(tab, request_id) 815 | if body: 816 | try: 817 | return body.decode('utf-8') 818 | except UnicodeDecodeError: 819 | import base64 820 | return base64.b64encode(body).decode('utf-8') 821 | return None 822 | 823 | 824 | @section_tool("network-debugging") 825 | async def modify_headers( 826 | instance_id: str, 827 | headers: Dict[str, str] 828 | ) -> bool: 829 | """ 830 | Modify request headers for future requests. 831 | 832 | Args: 833 | instance_id (str): Browser instance ID. 834 | headers (Dict[str, str]): Headers to add/modify. 835 | 836 | Returns: 837 | bool: True if modified successfully. 838 | """ 839 | tab = await browser_manager.get_tab(instance_id) 840 | if not tab: 841 | raise Exception(f"Instance not found: {instance_id}") 842 | return await network_interceptor.modify_headers(tab, headers) 843 | 844 | 845 | @section_tool("cookies-storage") 846 | async def get_cookies( 847 | instance_id: str, 848 | urls: Optional[List[str]] = None 849 | ) -> List[Dict[str, Any]]: 850 | """ 851 | Get cookies for current page or specific URLs. 852 | 853 | Args: 854 | instance_id (str): Browser instance ID. 855 | urls (Optional[List[str]]): Optional list of URLs to get cookies for. 856 | 857 | Returns: 858 | List[Dict[str, Any]]: List of cookies. 859 | """ 860 | tab = await browser_manager.get_tab(instance_id) 861 | if not tab: 862 | raise Exception(f"Instance not found: {instance_id}") 863 | return await network_interceptor.get_cookies(tab, urls) 864 | 865 | 866 | @section_tool("cookies-storage") 867 | async def set_cookie( 868 | instance_id: str, 869 | name: str, 870 | value: str, 871 | url: Optional[str] = None, 872 | domain: Optional[str] = None, 873 | path: str = "/", 874 | secure: bool = False, 875 | http_only: bool = False, 876 | same_site: Optional[str] = None 877 | ) -> bool: 878 | """ 879 | Set a cookie. 880 | 881 | Args: 882 | instance_id (str): Browser instance ID. 883 | name (str): Cookie name. 884 | value (str): Cookie value. 885 | url (Optional[str]): The request-URI to associate with the cookie. 886 | domain (Optional[str]): Cookie domain. 887 | path (str): Cookie path. 888 | secure (bool): Secure flag. 889 | http_only (bool): HttpOnly flag. 890 | same_site (Optional[str]): SameSite attribute ('Strict', 'Lax', or 'None'). 891 | 892 | Returns: 893 | bool: True if set successfully. 894 | """ 895 | tab = await browser_manager.get_tab(instance_id) 896 | if not tab: 897 | raise Exception(f"Instance not found: {instance_id}") 898 | 899 | if not url and not domain: 900 | current_url = tab.url if hasattr(tab, 'url') else None 901 | if current_url: 902 | url = current_url 903 | else: 904 | raise Exception("At least one of 'url' or 'domain' must be specified") 905 | 906 | cookie = { 907 | "name": name, 908 | "value": value, 909 | "path": path, 910 | "secure": secure, 911 | "http_only": http_only 912 | } 913 | if url: 914 | cookie["url"] = url 915 | if domain: 916 | cookie["domain"] = domain 917 | if same_site: 918 | cookie["same_site"] = same_site 919 | return await network_interceptor.set_cookie(tab, cookie) 920 | 921 | 922 | @section_tool("cookies-storage") 923 | async def clear_cookies( 924 | instance_id: str, 925 | url: Optional[str] = None 926 | ) -> bool: 927 | """ 928 | Clear cookies. 929 | 930 | Args: 931 | instance_id (str): Browser instance ID. 932 | url (Optional[str]): Optional URL to clear cookies for (clears all if not specified). 933 | 934 | Returns: 935 | bool: True if cleared successfully. 936 | """ 937 | tab = await browser_manager.get_tab(instance_id) 938 | if not tab: 939 | raise Exception(f"Instance not found: {instance_id}") 940 | return await network_interceptor.clear_cookies(tab, url) 941 | 942 | 943 | @mcp.resource("browser://{instance_id}/state") 944 | async def get_browser_state_resource(instance_id: str) -> str: 945 | """ 946 | Get current state of a browser instance. 947 | 948 | Args: 949 | instance_id (str): Browser instance ID. 950 | 951 | Returns: 952 | str: JSON string of the browser state or error message. 953 | """ 954 | state = await browser_manager.get_page_state(instance_id) 955 | if state: 956 | return json.dumps(state.dict(), indent=2) 957 | return json.dumps({"error": "Instance not found"}) 958 | 959 | 960 | @mcp.resource("browser://{instance_id}/cookies") 961 | async def get_cookies_resource(instance_id: str) -> str: 962 | """ 963 | Get cookies for a browser instance. 964 | 965 | Args: 966 | instance_id (str): Browser instance ID. 967 | 968 | Returns: 969 | str: JSON string of cookies or error message. 970 | """ 971 | tab = await browser_manager.get_tab(instance_id) 972 | if tab: 973 | cookies = await network_interceptor.get_cookies(tab) 974 | return json.dumps(cookies, indent=2) 975 | return json.dumps({"error": "Instance not found"}) 976 | 977 | 978 | @mcp.resource("browser://{instance_id}/network") 979 | async def get_network_resource(instance_id: str) -> str: 980 | """ 981 | Get network requests for a browser instance. 982 | 983 | Args: 984 | instance_id (str): Browser instance ID. 985 | 986 | Returns: 987 | str: JSON string of network requests. 988 | """ 989 | requests = await network_interceptor.list_requests(instance_id) 990 | return json.dumps([req.dict() for req in requests], indent=2) 991 | 992 | 993 | @mcp.resource("browser://{instance_id}/console") 994 | async def get_console_resource(instance_id: str) -> str: 995 | """ 996 | Get console logs for a browser instance. 997 | 998 | Args: 999 | instance_id (str): Browser instance ID. 1000 | 1001 | Returns: 1002 | str: JSON string of console logs or error message. 1003 | """ 1004 | state = await browser_manager.get_page_state(instance_id) 1005 | if state: 1006 | return json.dumps(state.console_logs, indent=2) 1007 | return json.dumps({"error": "Instance not found"}) 1008 | 1009 | 1010 | @section_tool("debugging") 1011 | async def get_debug_view( 1012 | max_errors: int = 50, 1013 | max_warnings: int = 50, 1014 | max_info: int = 50, 1015 | include_all: bool = False 1016 | ) -> Dict[str, Any]: 1017 | """ 1018 | Get comprehensive debug view with all logged errors and statistics. 1019 | 1020 | Args: 1021 | max_errors (int): Maximum number of errors to include (default: 50). 1022 | max_warnings (int): Maximum number of warnings to include (default: 50). 1023 | max_info (int): Maximum number of info logs to include (default: 50). 1024 | include_all (bool): Include all logs regardless of limits (default: False). 1025 | 1026 | Returns: 1027 | Dict[str, Any]: Debug information including errors, warnings, and statistics. 1028 | """ 1029 | debug_data = debug_logger.get_debug_view_paginated( 1030 | max_errors=max_errors if not include_all else None, 1031 | max_warnings=max_warnings if not include_all else None, 1032 | max_info=max_info if not include_all else None 1033 | ) 1034 | return debug_data 1035 | 1036 | 1037 | @section_tool("debugging") 1038 | async def clear_debug_view() -> bool: 1039 | """ 1040 | Clear all debug logs and statistics with timeout protection. 1041 | 1042 | Returns: 1043 | bool: True if cleared successfully. 1044 | """ 1045 | try: 1046 | await asyncio.wait_for( 1047 | asyncio.to_thread(debug_logger.clear_debug_view_safe), 1048 | timeout=10.0 1049 | ) 1050 | return True 1051 | except asyncio.TimeoutError: 1052 | return False 1053 | 1054 | 1055 | @section_tool("debugging") 1056 | async def export_debug_logs( 1057 | filename: str = "debug_log.json", 1058 | max_errors: int = 100, 1059 | max_warnings: int = 100, 1060 | max_info: int = 100, 1061 | include_all: bool = False, 1062 | format: str = "auto" 1063 | ) -> str: 1064 | """ 1065 | Export debug logs to a file using the fastest available method with timeout protection. 1066 | 1067 | Args: 1068 | filename (str): Name of the file to export to. 1069 | max_errors (int): Maximum number of errors to export (default: 100). 1070 | max_warnings (int): Maximum number of warnings to export (default: 100). 1071 | max_info (int): Maximum number of info logs to export (default: 100). 1072 | include_all (bool): Include all logs regardless of limits (default: False). 1073 | format (str): Export format: 'json', 'pickle', 'gzip-pickle', 'auto' (default: 'auto'). 1074 | 'auto' chooses fastest format based on data size: 1075 | - Small data (<100 items): JSON (human readable) 1076 | - Medium data (100-1000 items): Pickle (fast binary) 1077 | - Large data (>1000 items): Gzip-Pickle (fastest, compressed) 1078 | 1079 | Returns: 1080 | str: Path to the exported file. 1081 | """ 1082 | try: 1083 | filepath = await asyncio.wait_for( 1084 | asyncio.to_thread( 1085 | debug_logger.export_to_file_paginated, 1086 | filename, 1087 | max_errors if not include_all else None, 1088 | max_warnings if not include_all else None, 1089 | max_info if not include_all else None, 1090 | format 1091 | ), 1092 | timeout=30.0 1093 | ) 1094 | return filepath 1095 | except asyncio.TimeoutError: 1096 | return f"Export timeout - file too large. Try with smaller limits or 'gzip-pickle' format." 1097 | 1098 | 1099 | @section_tool("debugging") 1100 | async def get_debug_lock_status() -> Dict[str, Any]: 1101 | """ 1102 | Get current debug logger lock status for debugging hanging exports. 1103 | 1104 | Returns: 1105 | Dict[str, Any]: Lock status information. 1106 | """ 1107 | try: 1108 | return debug_logger.get_lock_status() 1109 | except Exception as e: 1110 | return {"error": str(e)} 1111 | 1112 | 1113 | @section_tool("tabs") 1114 | async def list_tabs(instance_id: str) -> List[Dict[str, str]]: 1115 | """ 1116 | List all tabs for a browser instance. 1117 | 1118 | Args: 1119 | instance_id (str): Browser instance ID. 1120 | 1121 | Returns: 1122 | List[Dict[str, str]]: List of tabs with their details. 1123 | """ 1124 | return await browser_manager.list_tabs(instance_id) 1125 | 1126 | 1127 | @section_tool("tabs") 1128 | async def switch_tab( 1129 | instance_id: str, 1130 | tab_id: str 1131 | ) -> bool: 1132 | """ 1133 | Switch to a specific tab by bringing it to front. 1134 | 1135 | Args: 1136 | instance_id (str): Browser instance ID. 1137 | tab_id (str): Target tab ID to switch to. 1138 | 1139 | Returns: 1140 | bool: True if switched successfully. 1141 | """ 1142 | return await browser_manager.switch_to_tab(instance_id, tab_id) 1143 | 1144 | 1145 | @section_tool("tabs") 1146 | async def close_tab( 1147 | instance_id: str, 1148 | tab_id: str 1149 | ) -> bool: 1150 | """ 1151 | Close a specific tab. 1152 | 1153 | Args: 1154 | instance_id (str): Browser instance ID. 1155 | tab_id (str): Tab ID to close. 1156 | 1157 | Returns: 1158 | bool: True if closed successfully. 1159 | """ 1160 | return await browser_manager.close_tab(instance_id, tab_id) 1161 | 1162 | 1163 | @section_tool("tabs") 1164 | async def get_active_tab(instance_id: str) -> Dict[str, Any]: 1165 | """ 1166 | Get information about the currently active tab. 1167 | 1168 | Args: 1169 | instance_id (str): Browser instance ID. 1170 | 1171 | Returns: 1172 | Dict[str, Any]: Active tab information. 1173 | """ 1174 | tab = await browser_manager.get_active_tab(instance_id) 1175 | if not tab: 1176 | return {"error": "No active tab found"} 1177 | await tab 1178 | return { 1179 | "tab_id": str(tab.target.target_id), 1180 | "url": getattr(tab, 'url', '') or '', 1181 | "title": getattr(tab.target, 'title', '') or 'Untitled', 1182 | "type": getattr(tab.target, 'type_', 'page') 1183 | } 1184 | 1185 | 1186 | @section_tool("tabs") 1187 | async def new_tab( 1188 | instance_id: str, 1189 | url: str = "about:blank" 1190 | ) -> Dict[str, Any]: 1191 | """ 1192 | Open a new tab in the browser instance. 1193 | 1194 | Args: 1195 | instance_id (str): Browser instance ID. 1196 | url (str): URL to open in the new tab. 1197 | 1198 | Returns: 1199 | Dict[str, Any]: New tab information. 1200 | """ 1201 | browser = await browser_manager.get_browser(instance_id) 1202 | if not browser: 1203 | raise Exception(f"Instance not found: {instance_id}") 1204 | try: 1205 | new_tab_obj = await browser.get(url, new_tab=True) 1206 | await new_tab_obj 1207 | return { 1208 | "tab_id": str(new_tab_obj.target.target_id), 1209 | "url": getattr(new_tab_obj, 'url', '') or url, 1210 | "title": getattr(new_tab_obj.target, 'title', '') or 'New Tab', 1211 | "type": getattr(new_tab_obj.target, 'type_', 'page') 1212 | } 1213 | except Exception as e: 1214 | raise Exception(f"Failed to create new tab: {str(e)}") 1215 | 1216 | 1217 | @section_tool("element-extraction") 1218 | async def extract_element_styles( 1219 | instance_id: str, 1220 | selector: str, 1221 | include_computed: bool = True, 1222 | include_css_rules: bool = True, 1223 | include_pseudo: bool = True, 1224 | include_inheritance: bool = False 1225 | ) -> Dict[str, Any]: 1226 | """ 1227 | Extract complete styling information from an element. 1228 | 1229 | Args: 1230 | instance_id (str): Browser instance ID. 1231 | selector (str): CSS selector for the element. 1232 | include_computed (bool): Include computed styles. 1233 | include_css_rules (bool): Include matching CSS rules. 1234 | include_pseudo (bool): Include pseudo-element styles (::before, ::after). 1235 | include_inheritance (bool): Include style inheritance chain. 1236 | 1237 | Returns: 1238 | Dict[str, Any]: Complete styling data including computed styles, CSS rules, pseudo-elements. 1239 | """ 1240 | tab = await browser_manager.get_tab(instance_id) 1241 | if not tab: 1242 | raise Exception(f"Instance not found: {instance_id}") 1243 | return await element_cloner.extract_element_styles( 1244 | tab, 1245 | selector=selector, 1246 | include_computed=include_computed, 1247 | include_css_rules=include_css_rules, 1248 | include_pseudo=include_pseudo, 1249 | include_inheritance=include_inheritance 1250 | ) 1251 | 1252 | 1253 | @section_tool("element-extraction") 1254 | async def extract_element_structure( 1255 | instance_id: str, 1256 | selector: str, 1257 | include_children: bool = False, 1258 | include_attributes: bool = True, 1259 | include_data_attributes: bool = True, 1260 | max_depth: int = 3 1261 | ) -> Dict[str, Any]: 1262 | """ 1263 | Extract complete HTML structure and DOM information. 1264 | 1265 | Args: 1266 | instance_id (str): Browser instance ID. 1267 | selector (str): CSS selector for the element. 1268 | include_children (bool): Include child elements. 1269 | include_attributes (bool): Include all attributes. 1270 | include_data_attributes (bool): Include data-* attributes specifically. 1271 | max_depth (int): Maximum depth for children extraction. 1272 | 1273 | Returns: 1274 | Dict[str, Any]: HTML structure, attributes, position, and children data. 1275 | """ 1276 | tab = await browser_manager.get_tab(instance_id) 1277 | if not tab: 1278 | raise Exception(f"Instance not found: {instance_id}") 1279 | return await element_cloner.extract_element_structure( 1280 | tab, 1281 | selector=selector, 1282 | include_children=include_children, 1283 | include_attributes=include_attributes, 1284 | include_data_attributes=include_data_attributes, 1285 | max_depth=max_depth 1286 | ) 1287 | 1288 | 1289 | @section_tool("element-extraction") 1290 | async def extract_element_events( 1291 | instance_id: str, 1292 | selector: str, 1293 | include_inline: bool = True, 1294 | include_listeners: bool = True, 1295 | include_framework: bool = True, 1296 | analyze_handlers: bool = False 1297 | ) -> Dict[str, Any]: 1298 | """ 1299 | Extract complete event listener and JavaScript handler information. 1300 | 1301 | Args: 1302 | instance_id (str): Browser instance ID. 1303 | selector (str): CSS selector for the element. 1304 | include_inline (bool): Include inline event handlers (onclick, etc.). 1305 | include_listeners (bool): Include addEventListener attached handlers. 1306 | include_framework (bool): Include framework-specific handlers (React, Vue, etc.). 1307 | analyze_handlers (bool): Analyze handler functions for full details (can be large). 1308 | 1309 | Returns: 1310 | Dict[str, Any]: Event listeners, inline handlers, framework handlers, detected frameworks. 1311 | """ 1312 | tab = await browser_manager.get_tab(instance_id) 1313 | if not tab: 1314 | raise Exception(f"Instance not found: {instance_id}") 1315 | return await element_cloner.extract_element_events( 1316 | tab, 1317 | selector=selector, 1318 | include_inline=include_inline, 1319 | include_listeners=include_listeners, 1320 | include_framework=include_framework, 1321 | analyze_handlers=analyze_handlers 1322 | ) 1323 | 1324 | 1325 | @section_tool("element-extraction") 1326 | async def extract_element_animations( 1327 | instance_id: str, 1328 | selector: str, 1329 | include_css_animations: bool = True, 1330 | include_transitions: bool = True, 1331 | include_transforms: bool = True, 1332 | analyze_keyframes: bool = True 1333 | ) -> Dict[str, Any]: 1334 | """ 1335 | Extract CSS animations, transitions, and transforms. 1336 | 1337 | Args: 1338 | instance_id (str): Browser instance ID. 1339 | selector (str): CSS selector for the element. 1340 | include_css_animations (bool): Include CSS @keyframes animations. 1341 | include_transitions (bool): Include CSS transitions. 1342 | include_transforms (bool): Include CSS transforms. 1343 | analyze_keyframes (bool): Analyze keyframe rules. 1344 | 1345 | Returns: 1346 | Dict[str, Any]: Animation data, transition data, transform data, keyframe rules. 1347 | """ 1348 | tab = await browser_manager.get_tab(instance_id) 1349 | if not tab: 1350 | raise Exception(f"Instance not found: {instance_id}") 1351 | return await element_cloner.extract_element_animations( 1352 | tab, 1353 | selector=selector, 1354 | include_css_animations=include_css_animations, 1355 | include_transitions=include_transitions, 1356 | include_transforms=include_transforms, 1357 | analyze_keyframes=analyze_keyframes 1358 | ) 1359 | 1360 | 1361 | @section_tool("element-extraction") 1362 | async def extract_element_assets( 1363 | instance_id: str, 1364 | selector: str, 1365 | include_images: bool = True, 1366 | include_backgrounds: bool = True, 1367 | include_fonts: bool = True, 1368 | fetch_external: bool = False 1369 | ) -> Dict[str, Any]: 1370 | """ 1371 | Extract all assets related to an element (images, fonts, etc.). 1372 | 1373 | Args: 1374 | instance_id (str): Browser instance ID. 1375 | selector (str): CSS selector for the element. 1376 | include_images (bool): Include img src and related images. 1377 | include_backgrounds (bool): Include background images. 1378 | include_fonts (bool): Include font information. 1379 | fetch_external (bool): Whether to fetch external assets for analysis. 1380 | 1381 | Returns: 1382 | Dict[str, Any]: Images, background images, fonts, icons, videos, audio assets. 1383 | """ 1384 | tab = await browser_manager.get_tab(instance_id) 1385 | if not tab: 1386 | raise Exception(f"Instance not found: {instance_id}") 1387 | result = await element_cloner.extract_element_assets( 1388 | tab, 1389 | selector=selector, 1390 | include_images=include_images, 1391 | include_backgrounds=include_backgrounds, 1392 | include_fonts=include_fonts, 1393 | fetch_external=fetch_external 1394 | ) 1395 | return await response_handler.handle_response(result, f"element_assets_{instance_id}_{selector.replace(' ', '_')}") 1396 | 1397 | 1398 | @section_tool("element-extraction") 1399 | async def extract_element_styles_cdp( 1400 | instance_id: str, 1401 | selector: str, 1402 | include_computed: bool = True, 1403 | include_css_rules: bool = True, 1404 | include_pseudo: bool = True, 1405 | include_inheritance: bool = False, 1406 | ) -> Dict[str, Any]: 1407 | """ 1408 | Extract element styles using direct CDP calls (no JavaScript evaluation). 1409 | This prevents hanging issues by using nodriver's native CDP methods. 1410 | 1411 | Args: 1412 | instance_id (str): Browser instance ID 1413 | selector (str): CSS selector for the element 1414 | include_computed (bool): Include computed styles 1415 | include_css_rules (bool): Include matching CSS rules 1416 | include_pseudo (bool): Include pseudo-element styles 1417 | include_inheritance (bool): Include style inheritance chain 1418 | 1419 | Returns: 1420 | Dict[str, Any]: Styling data extracted using CDP 1421 | """ 1422 | tab = await browser_manager.get_tab(instance_id) 1423 | if not tab: 1424 | raise Exception(f"Instance not found: {instance_id}") 1425 | return await element_cloner.extract_element_styles_cdp( 1426 | tab, 1427 | selector=selector, 1428 | include_computed=include_computed, 1429 | include_css_rules=include_css_rules, 1430 | include_pseudo=include_pseudo, 1431 | include_inheritance=include_inheritance 1432 | ) 1433 | 1434 | 1435 | @section_tool("element-extraction") 1436 | async def extract_related_files( 1437 | instance_id: str, 1438 | analyze_css: bool = True, 1439 | analyze_js: bool = True, 1440 | follow_imports: bool = False, 1441 | max_depth: int = 2 1442 | ) -> Dict[str, Any]: 1443 | """ 1444 | Discover and analyze related CSS/JS files for context. 1445 | 1446 | Args: 1447 | instance_id (str): Browser instance ID. 1448 | analyze_css (bool): Analyze linked CSS files. 1449 | analyze_js (bool): Analyze linked JS files. 1450 | follow_imports (bool): Follow @import and module imports (uses network). 1451 | max_depth (int): Maximum depth for following imports. 1452 | 1453 | Returns: 1454 | Dict[str, Any]: Stylesheets, scripts, imports, modules, framework detection. 1455 | """ 1456 | tab = await browser_manager.get_tab(instance_id) 1457 | if not tab: 1458 | raise Exception(f"Instance not found: {instance_id}") 1459 | result = await element_cloner.extract_related_files( 1460 | tab, 1461 | analyze_css=analyze_css, 1462 | analyze_js=analyze_js, 1463 | follow_imports=follow_imports, 1464 | max_depth=max_depth 1465 | ) 1466 | return await response_handler.handle_response(result, f"related_files_{instance_id}") 1467 | 1468 | 1469 | @section_tool("element-extraction") 1470 | async def clone_element_complete( 1471 | instance_id: str, 1472 | selector: str, 1473 | extraction_options: Optional[str] = None 1474 | ) -> Dict[str, Any]: 1475 | """ 1476 | Master function that extracts ALL element data using specialized functions. 1477 | 1478 | This is the ultimate element cloning tool that combines all extraction methods. 1479 | Use this when you want complete element fidelity for recreation or analysis. 1480 | 1481 | Args: 1482 | instance_id (str): Browser instance ID. 1483 | selector (str): CSS selector for the element. 1484 | extraction_options (Optional[str]): Dict specifying what to extract and options for each. 1485 | Example: { 1486 | 'styles': {'include_computed': True, 'include_pseudo': True}, 1487 | 'structure': {'include_children': True, 'max_depth': 2}, 1488 | 'events': {'include_framework': True, 'analyze_handlers': False}, 1489 | 'animations': {'analyze_keyframes': True}, 1490 | 'assets': {'fetch_external': False}, 1491 | 'related_files': {'follow_imports': True, 'max_depth': 1} 1492 | } 1493 | 1494 | Returns: 1495 | Dict[str, Any]: Complete element clone with styles, structure, events, animations, assets, related files. 1496 | """ 1497 | parsed_options = None 1498 | if extraction_options: 1499 | try: 1500 | parsed_options = json.loads(extraction_options) 1501 | except json.JSONDecodeError: 1502 | raise Exception(f"Invalid JSON in extraction_options: {extraction_options}") 1503 | tab = await browser_manager.get_tab(instance_id) 1504 | if not tab: 1505 | raise Exception(f"Instance not found: {instance_id}") 1506 | result = await comprehensive_element_cloner.extract_complete_element( 1507 | tab, 1508 | selector=selector, 1509 | include_children=parsed_options.get('structure', {}).get('include_children', True) if parsed_options else True 1510 | ) 1511 | 1512 | return response_handler.handle_response( 1513 | result, 1514 | fallback_filename_prefix="complete_clone", 1515 | metadata={ 1516 | "selector": selector, 1517 | "extraction_options": parsed_options, 1518 | "url": getattr(tab, 'url', 'unknown') 1519 | } 1520 | ) 1521 | 1522 | 1523 | @section_tool("debugging") 1524 | async def hot_reload() -> str: 1525 | """ 1526 | Hot reload all modules without restarting the server. 1527 | 1528 | Returns: 1529 | str: Status message. 1530 | """ 1531 | try: 1532 | modules_to_reload = [ 1533 | 'browser_manager', 1534 | 'network_interceptor', 1535 | 'dom_handler', 1536 | 'debug_logger', 1537 | 'models' 1538 | ] 1539 | reloaded_modules = [] 1540 | for module_name in modules_to_reload: 1541 | if module_name in sys.modules: 1542 | importlib.reload(sys.modules[module_name]) 1543 | reloaded_modules.append(module_name) 1544 | if module_name == 'browser_manager': 1545 | global browser_manager, BrowserManager 1546 | browser_manager = BrowserManager() 1547 | elif module_name == 'network_interceptor': 1548 | global network_interceptor, NetworkInterceptor 1549 | network_interceptor = NetworkInterceptor() 1550 | elif module_name == 'dom_handler': 1551 | global dom_handler, DOMHandler 1552 | dom_handler = DOMHandler() 1553 | elif module_name == 'debug_logger': 1554 | global debug_logger 1555 | from debug_logger import debug_logger 1556 | return f"✅ Hot reload completed! Reloaded modules: {', '.join(reloaded_modules)}" 1557 | except Exception as e: 1558 | return f"❌ Hot reload failed: {str(e)}" 1559 | 1560 | 1561 | @section_tool("debugging") 1562 | async def reload_status() -> str: 1563 | """ 1564 | Check the status of loaded modules. 1565 | 1566 | Returns: 1567 | str: Module status information. 1568 | """ 1569 | try: 1570 | modules_info = [] 1571 | modules_to_check = [ 1572 | 'browser_manager', 1573 | 'network_interceptor', 1574 | 'dom_handler', 1575 | 'debug_logger', 1576 | 'models', 1577 | 'persistent_storage' 1578 | ] 1579 | for module_name in modules_to_check: 1580 | if module_name in sys.modules: 1581 | module = sys.modules[module_name] 1582 | modules_info.append(f"✅ {module_name}: {getattr(module, '__file__', 'built-in')}") 1583 | else: 1584 | modules_info.append(f"❌ {module_name}: Not loaded") 1585 | return "\n".join(modules_info) 1586 | except Exception as e: 1587 | return f"Error checking module status: {str(e)}" 1588 | 1589 | 1590 | @section_tool("debugging") 1591 | async def validate_browser_environment_tool() -> Dict[str, Any]: 1592 | """ 1593 | Validate browser environment and diagnose potential issues. 1594 | 1595 | Returns: 1596 | Dict[str, Any]: Environment validation results with platform info and recommendations 1597 | """ 1598 | try: 1599 | return validate_browser_environment() 1600 | except Exception as e: 1601 | return { 1602 | "error": str(e), 1603 | "platform_info": get_platform_info(), 1604 | "is_ready": False, 1605 | "issues": [f"Validation failed: {str(e)}"], 1606 | "warnings": [] 1607 | } 1608 | 1609 | 1610 | @section_tool("progressive-cloning") 1611 | async def clone_element_progressive( 1612 | instance_id: str, 1613 | selector: str, 1614 | include_children: bool = True 1615 | ) -> Dict[str, Any]: 1616 | """ 1617 | Clone element progressively - returns lightweight base structure with element_id. 1618 | 1619 | Args: 1620 | instance_id (str): Browser instance ID. 1621 | selector (str): CSS selector for the element. 1622 | include_children (bool): Whether to extract child elements. 1623 | 1624 | Returns: 1625 | Dict[str, Any]: Base structure with element_id for progressive expansion. 1626 | """ 1627 | tab = await browser_manager.get_tab(instance_id) 1628 | if not tab: 1629 | raise Exception(f"Instance not found: {instance_id}") 1630 | return await progressive_element_cloner.clone_element_progressive(tab, selector, include_children) 1631 | 1632 | 1633 | @section_tool("progressive-cloning") 1634 | async def expand_styles( 1635 | element_id: str, 1636 | categories: Optional[List[str]] = None, 1637 | properties: Optional[List[str]] = None 1638 | ) -> Dict[str, Any]: 1639 | """ 1640 | Expand styles data for a stored element. 1641 | 1642 | Args: 1643 | element_id (str): Element ID from clone_element_progressive(). 1644 | categories (Optional[List[str]]): Style categories to include (layout, typography, colors, spacing, borders, backgrounds, effects, animation). 1645 | properties (Optional[List[str]]): Specific CSS property names to include. 1646 | 1647 | Returns: 1648 | Dict[str, Any]: Filtered styles data. 1649 | """ 1650 | return progressive_element_cloner.expand_styles(element_id, categories, properties) 1651 | 1652 | 1653 | @section_tool("progressive-cloning") 1654 | async def expand_events( 1655 | element_id: str, 1656 | event_types: Optional[List[str]] = None 1657 | ) -> Dict[str, Any]: 1658 | """ 1659 | Expand event listeners data for a stored element. 1660 | 1661 | Args: 1662 | element_id (str): Element ID from clone_element_progressive(). 1663 | event_types (Optional[List[str]]): Event types or sources to include (click, react, inline, addEventListener). 1664 | 1665 | Returns: 1666 | Dict[str, Any]: Filtered event listeners data. 1667 | """ 1668 | return progressive_element_cloner.expand_events(element_id, event_types) 1669 | 1670 | 1671 | @section_tool("progressive-cloning") 1672 | async def expand_children( 1673 | element_id: str, 1674 | depth_range: Optional[List] = None, 1675 | max_count: Optional[Any] = None 1676 | ) -> Dict[str, Any]: 1677 | """ 1678 | Expand children data for a stored element. 1679 | 1680 | Args: 1681 | element_id (str): Element ID from clone_element_progressive(). 1682 | depth_range (Optional[List]): [min_depth, max_depth] range to include. 1683 | max_count (Optional[Any]): Maximum number of children to return. 1684 | 1685 | Returns: 1686 | Dict[str, Any]: Filtered children data. 1687 | """ 1688 | if isinstance(max_count, str): 1689 | try: 1690 | max_count = int(max_count) if max_count else None 1691 | except ValueError: 1692 | return {"error": f"Invalid max_count value: {max_count}"} 1693 | 1694 | if isinstance(depth_range, list): 1695 | try: 1696 | depth_range = [int(x) if isinstance(x, str) else x for x in depth_range] 1697 | except ValueError: 1698 | return {"error": f"Invalid depth_range values: {depth_range}"} 1699 | 1700 | depth_tuple = tuple(depth_range) if depth_range else None 1701 | 1702 | result = progressive_element_cloner.expand_children(element_id, depth_tuple, max_count) 1703 | return response_handler.handle_response(result, f"expand_children_{element_id}") 1704 | 1705 | 1706 | @section_tool("progressive-cloning") 1707 | async def expand_css_rules( 1708 | element_id: str, 1709 | source_types: Optional[List[str]] = None 1710 | ) -> Dict[str, Any]: 1711 | """ 1712 | Expand CSS rules data for a stored element. 1713 | 1714 | Args: 1715 | element_id (str): Element ID from clone_element_progressive(). 1716 | source_types (Optional[List[str]]): CSS rule sources to include (inline, external stylesheet URLs). 1717 | 1718 | Returns: 1719 | Dict[str, Any]: Filtered CSS rules data. 1720 | """ 1721 | return progressive_element_cloner.expand_css_rules(element_id, source_types) 1722 | 1723 | 1724 | @section_tool("progressive-cloning") 1725 | async def expand_pseudo_elements( 1726 | element_id: str 1727 | ) -> Dict[str, Any]: 1728 | """ 1729 | Expand pseudo-elements data for a stored element. 1730 | 1731 | Args: 1732 | element_id (str): Element ID from clone_element_progressive(). 1733 | 1734 | Returns: 1735 | Dict[str, Any]: Pseudo-elements data (::before, ::after, etc.). 1736 | """ 1737 | return progressive_element_cloner.expand_pseudo_elements(element_id) 1738 | 1739 | 1740 | @section_tool("progressive-cloning") 1741 | async def expand_animations( 1742 | element_id: str 1743 | ) -> Dict[str, Any]: 1744 | """ 1745 | Expand animations and fonts data for a stored element. 1746 | 1747 | Args: 1748 | element_id (str): Element ID from clone_element_progressive(). 1749 | 1750 | Returns: 1751 | Dict[str, Any]: Animations, transitions, and fonts data. 1752 | """ 1753 | return progressive_element_cloner.expand_animations(element_id) 1754 | 1755 | 1756 | @section_tool("progressive-cloning") 1757 | async def list_stored_elements() -> Dict[str, Any]: 1758 | """ 1759 | List all stored elements with their basic info. 1760 | 1761 | Returns: 1762 | Dict[str, Any]: List of stored elements with metadata. 1763 | """ 1764 | return progressive_element_cloner.list_stored_elements() 1765 | 1766 | 1767 | @section_tool("progressive-cloning") 1768 | async def clear_stored_element( 1769 | element_id: str 1770 | ) -> Dict[str, Any]: 1771 | """ 1772 | Clear a specific stored element. 1773 | 1774 | Args: 1775 | element_id (str): Element ID to clear. 1776 | 1777 | Returns: 1778 | Dict[str, Any]: Success/error message. 1779 | """ 1780 | return progressive_element_cloner.clear_stored_element(element_id) 1781 | 1782 | 1783 | @section_tool("progressive-cloning") 1784 | async def clear_all_elements() -> Dict[str, Any]: 1785 | """ 1786 | Clear all stored elements. 1787 | 1788 | Returns: 1789 | Dict[str, Any]: Success message. 1790 | """ 1791 | return progressive_element_cloner.clear_all_elements() 1792 | 1793 | 1794 | @section_tool("file-extraction") 1795 | async def clone_element_to_file( 1796 | instance_id: str, 1797 | selector: str, 1798 | extraction_options: Optional[str] = None 1799 | ) -> Dict[str, Any]: 1800 | """ 1801 | Clone element completely and save to file, returning file path instead of full data. 1802 | 1803 | This is ideal when you want complete element data but don't want to overwhelm 1804 | the response with large JSON objects. The data is saved to a JSON file that 1805 | can be read later. 1806 | 1807 | Args: 1808 | instance_id (str): Browser instance ID. 1809 | selector (str): CSS selector for the element. 1810 | extraction_options (Optional[str]): JSON string with extraction options. 1811 | 1812 | Returns: 1813 | Dict[str, Any]: File path and summary information about the cloned element. 1814 | """ 1815 | tab = await browser_manager.get_tab(instance_id) 1816 | if not tab: 1817 | raise Exception(f"Instance not found: {instance_id}") 1818 | parsed_options = None 1819 | if extraction_options: 1820 | try: 1821 | parsed_options = json.loads(extraction_options) 1822 | except json.JSONDecodeError: 1823 | return {"error": "Invalid extraction_options JSON"} 1824 | return await file_based_element_cloner.clone_element_complete_to_file( 1825 | tab, selector=selector, extraction_options=parsed_options 1826 | ) 1827 | 1828 | 1829 | @section_tool("file-extraction") 1830 | async def extract_complete_element_to_file( 1831 | instance_id: str, 1832 | selector: str, 1833 | include_children: bool = True 1834 | ) -> Dict[str, Any]: 1835 | """ 1836 | Extract complete element using working comprehensive cloner and save to file. 1837 | 1838 | This uses the proven comprehensive extraction logic that returns large amounts 1839 | of data, but saves it to a file instead of overwhelming the response. 1840 | 1841 | Args: 1842 | instance_id (str): Browser instance ID. 1843 | selector (str): CSS selector for the element. 1844 | include_children (bool): Whether to include child elements. 1845 | 1846 | Returns: 1847 | Dict[str, Any]: File path and concise summary instead of massive data dump. 1848 | """ 1849 | tab = await browser_manager.get_tab(instance_id) 1850 | if not tab: 1851 | raise Exception(f"Instance not found: {instance_id}") 1852 | return await file_based_element_cloner.extract_complete_element_to_file( 1853 | tab, selector, include_children 1854 | ) 1855 | 1856 | 1857 | @section_tool("element-extraction") 1858 | async def extract_complete_element_cdp( 1859 | instance_id: str, 1860 | selector: str, 1861 | include_children: bool = True 1862 | ) -> Dict[str, Any]: 1863 | """ 1864 | Extract complete element using native CDP methods for 100% accuracy. 1865 | 1866 | This uses Chrome DevTools Protocol's native methods to extract: 1867 | - Complete computed styles via CSS.getComputedStyleForNode 1868 | - Matched CSS rules via CSS.getMatchedStylesForNode 1869 | - Event listeners via DOMDebugger.getEventListeners 1870 | - Complete DOM structure and attributes 1871 | 1872 | This provides the most accurate element cloning possible by bypassing 1873 | JavaScript limitations and using CDP's direct browser access. 1874 | 1875 | Args: 1876 | instance_id (str): Browser instance ID. 1877 | selector (str): CSS selector for the element. 1878 | include_children (bool): Whether to include child elements. 1879 | 1880 | Returns: 1881 | Dict[str, Any]: Complete element data with 100% accuracy. 1882 | """ 1883 | tab = await browser_manager.get_tab(instance_id) 1884 | if not tab: 1885 | raise Exception(f"Instance not found: {instance_id}") 1886 | cdp_cloner = CDPElementCloner() 1887 | return await cdp_cloner.extract_complete_element_cdp(tab, selector, include_children) 1888 | 1889 | 1890 | @section_tool("file-extraction") 1891 | async def extract_element_styles_to_file( 1892 | instance_id: str, 1893 | selector: str, 1894 | include_computed: bool = True, 1895 | include_css_rules: bool = True, 1896 | include_pseudo: bool = True, 1897 | include_inheritance: bool = False 1898 | ) -> Dict[str, Any]: 1899 | """ 1900 | Extract element styles and save to file, returning file path. 1901 | 1902 | Args: 1903 | instance_id (str): Browser instance ID. 1904 | selector (str): CSS selector for the element. 1905 | include_computed (bool): Include computed styles. 1906 | include_css_rules (bool): Include matching CSS rules. 1907 | include_pseudo (bool): Include pseudo-element styles. 1908 | include_inheritance (bool): Include style inheritance chain. 1909 | 1910 | Returns: 1911 | Dict[str, Any]: File path and summary of extracted styles. 1912 | """ 1913 | tab = await browser_manager.get_tab(instance_id) 1914 | if not tab: 1915 | raise Exception(f"Instance not found: {instance_id}") 1916 | return await file_based_element_cloner.extract_element_styles_to_file( 1917 | tab, 1918 | selector=selector, 1919 | include_computed=include_computed, 1920 | include_css_rules=include_css_rules, 1921 | include_pseudo=include_pseudo, 1922 | include_inheritance=include_inheritance 1923 | ) 1924 | 1925 | 1926 | @section_tool("file-extraction") 1927 | async def extract_element_structure_to_file( 1928 | instance_id: str, 1929 | selector: str, 1930 | include_children: bool = False, 1931 | include_attributes: bool = True, 1932 | include_data_attributes: bool = True, 1933 | max_depth: int = 3 1934 | ) -> Dict[str, Any]: 1935 | """ 1936 | Extract element structure and save to file, returning file path. 1937 | 1938 | Args: 1939 | instance_id (str): Browser instance ID. 1940 | selector (str): CSS selector for the element. 1941 | include_children (bool): Include child elements. 1942 | include_attributes (bool): Include all attributes. 1943 | include_data_attributes (bool): Include data-* attributes. 1944 | max_depth (int): Maximum depth for children extraction. 1945 | 1946 | Returns: 1947 | Dict[str, Any]: File path and summary of extracted structure. 1948 | """ 1949 | tab = await browser_manager.get_tab(instance_id) 1950 | if not tab: 1951 | raise Exception(f"Instance not found: {instance_id}") 1952 | return await file_based_element_cloner.extract_element_structure_to_file( 1953 | tab, 1954 | selector=selector, 1955 | include_children=include_children, 1956 | include_attributes=include_attributes, 1957 | include_data_attributes=include_data_attributes, 1958 | max_depth=max_depth 1959 | ) 1960 | 1961 | 1962 | @section_tool("file-extraction") 1963 | async def extract_element_events_to_file( 1964 | instance_id: str, 1965 | selector: str, 1966 | include_inline: bool = True, 1967 | include_listeners: bool = True, 1968 | include_framework: bool = True, 1969 | analyze_handlers: bool = True 1970 | ) -> Dict[str, Any]: 1971 | """ 1972 | Extract element events and save to file, returning file path. 1973 | 1974 | Args: 1975 | instance_id (str): Browser instance ID. 1976 | selector (str): CSS selector for the element. 1977 | include_inline (bool): Include inline event handlers. 1978 | include_listeners (bool): Include addEventListener handlers. 1979 | include_framework (bool): Include framework-specific handlers. 1980 | analyze_handlers (bool): Analyze handler functions. 1981 | 1982 | Returns: 1983 | Dict[str, Any]: File path and summary of extracted events. 1984 | """ 1985 | tab = await browser_manager.get_tab(instance_id) 1986 | if not tab: 1987 | raise Exception(f"Instance not found: {instance_id}") 1988 | return await file_based_element_cloner.extract_element_events_to_file( 1989 | tab, 1990 | selector=selector, 1991 | include_inline=include_inline, 1992 | include_listeners=include_listeners, 1993 | include_framework=include_framework, 1994 | analyze_handlers=analyze_handlers 1995 | ) 1996 | 1997 | 1998 | @section_tool("file-extraction") 1999 | async def extract_element_animations_to_file( 2000 | instance_id: str, 2001 | selector: str, 2002 | include_css_animations: bool = True, 2003 | include_transitions: bool = True, 2004 | include_transforms: bool = True, 2005 | analyze_keyframes: bool = True 2006 | ) -> Dict[str, Any]: 2007 | """ 2008 | Extract element animations and save to file, returning file path. 2009 | 2010 | Args: 2011 | instance_id (str): Browser instance ID. 2012 | selector (str): CSS selector for the element. 2013 | include_css_animations (bool): Include CSS animations. 2014 | include_transitions (bool): Include CSS transitions. 2015 | include_transforms (bool): Include CSS transforms. 2016 | analyze_keyframes (bool): Analyze keyframe rules. 2017 | 2018 | Returns: 2019 | Dict[str, Any]: File path and summary of extracted animations. 2020 | """ 2021 | tab = await browser_manager.get_tab(instance_id) 2022 | if not tab: 2023 | raise Exception(f"Instance not found: {instance_id}") 2024 | return await file_based_element_cloner.extract_element_animations_to_file( 2025 | tab, 2026 | selector=selector, 2027 | include_css_animations=include_css_animations, 2028 | include_transitions=include_transitions, 2029 | include_transforms=include_transforms, 2030 | analyze_keyframes=analyze_keyframes 2031 | ) 2032 | 2033 | 2034 | @section_tool("file-extraction") 2035 | async def extract_element_assets_to_file( 2036 | instance_id: str, 2037 | selector: str, 2038 | include_images: bool = True, 2039 | include_backgrounds: bool = True, 2040 | include_fonts: bool = True, 2041 | fetch_external: bool = False 2042 | ) -> Dict[str, Any]: 2043 | """ 2044 | Extract element assets and save to file, returning file path. 2045 | 2046 | Args: 2047 | instance_id (str): Browser instance ID. 2048 | selector (str): CSS selector for the element. 2049 | include_images (bool): Include images. 2050 | include_backgrounds (bool): Include background images. 2051 | include_fonts (bool): Include font information. 2052 | fetch_external (bool): Fetch external assets. 2053 | 2054 | Returns: 2055 | Dict[str, Any]: File path and summary of extracted assets. 2056 | """ 2057 | tab = await browser_manager.get_tab(instance_id) 2058 | if not tab: 2059 | raise Exception(f"Instance not found: {instance_id}") 2060 | return await file_based_element_cloner.extract_element_assets_to_file( 2061 | tab, 2062 | selector=selector, 2063 | include_images=include_images, 2064 | include_backgrounds=include_backgrounds, 2065 | include_fonts=include_fonts, 2066 | fetch_external=fetch_external 2067 | ) 2068 | 2069 | 2070 | @section_tool("file-extraction") 2071 | async def list_clone_files() -> List[Dict[str, Any]]: 2072 | """ 2073 | List all element clone files saved to disk. 2074 | 2075 | Returns: 2076 | List[Dict[str, Any]]: List of clone files with metadata and file information. 2077 | """ 2078 | return file_based_element_cloner.list_clone_files() 2079 | 2080 | 2081 | @section_tool("file-extraction") 2082 | async def cleanup_clone_files( 2083 | max_age_hours: int = 24 2084 | ) -> Dict[str, int]: 2085 | """ 2086 | Clean up old clone files to save disk space. 2087 | 2088 | Args: 2089 | max_age_hours (int): Maximum age in hours for files to keep. 2090 | 2091 | Returns: 2092 | Dict[str, int]: Number of files deleted. 2093 | """ 2094 | deleted_count = file_based_element_cloner.cleanup_old_files(max_age_hours) 2095 | return {"deleted_count": deleted_count} 2096 | 2097 | 2098 | @section_tool("cdp-functions") 2099 | async def list_cdp_commands() -> List[str]: 2100 | """ 2101 | List all available CDP Runtime commands for function execution. 2102 | 2103 | Returns: 2104 | List[str]: List of available CDP command names. 2105 | """ 2106 | return await cdp_function_executor.list_cdp_commands() 2107 | 2108 | 2109 | @section_tool("cdp-functions") 2110 | async def execute_cdp_command( 2111 | instance_id: str, 2112 | command: str, 2113 | params: Dict[str, Any] = None 2114 | ) -> Dict[str, Any]: 2115 | """ 2116 | Execute any CDP Runtime command with given parameters. 2117 | 2118 | Args: 2119 | instance_id (str): Browser instance ID. 2120 | command (str): CDP command name (e.g., 'evaluate', 'callFunctionOn'). 2121 | params (Dict[str, Any], optional): Command parameters as a dictionary. 2122 | IMPORTANT: Use snake_case parameter names (e.g., 'return_by_value') 2123 | NOT camelCase ('returnByValue'). The nodriver library expects 2124 | Python-style parameter names. 2125 | 2126 | Returns: 2127 | Dict[str, Any]: Command execution result. 2128 | 2129 | Example: 2130 | # Correct - use snake_case 2131 | params = {"expression": "document.title", "return_by_value": True} 2132 | 2133 | params = {"expression": "document.title", "returnByValue": True} 2134 | """ 2135 | tab = await browser_manager.get_tab(instance_id) 2136 | if not tab: 2137 | return {"success": False, "error": f"Instance not found: {instance_id}"} 2138 | return await cdp_function_executor.execute_cdp_command(tab, command, params or {}) 2139 | 2140 | 2141 | @section_tool("cdp-functions") 2142 | async def get_execution_contexts( 2143 | instance_id: str 2144 | ) -> List[Dict[str, Any]]: 2145 | """ 2146 | Get all available JavaScript execution contexts. 2147 | 2148 | Args: 2149 | instance_id (str): Browser instance ID. 2150 | 2151 | Returns: 2152 | List[Dict[str, Any]]: List of execution contexts with their details. 2153 | """ 2154 | tab = await browser_manager.get_tab(instance_id) 2155 | if not tab: 2156 | return [] 2157 | contexts = await cdp_function_executor.get_execution_contexts(tab) 2158 | return [ 2159 | { 2160 | "id": ctx.id, 2161 | "name": ctx.name, 2162 | "origin": ctx.origin, 2163 | "unique_id": ctx.unique_id, 2164 | "aux_data": ctx.aux_data 2165 | } 2166 | for ctx in contexts 2167 | ] 2168 | 2169 | 2170 | @section_tool("cdp-functions") 2171 | async def discover_global_functions( 2172 | instance_id: str, 2173 | context_id: str = None 2174 | ) -> List[Dict[str, Any]]: 2175 | """ 2176 | Discover all global JavaScript functions available in the page. 2177 | 2178 | Args: 2179 | instance_id (str): Browser instance ID. 2180 | context_id (str, optional): Optional execution context ID. 2181 | 2182 | Returns: 2183 | List[Dict[str, Any]]: List of discovered functions with their details. 2184 | """ 2185 | tab = await browser_manager.get_tab(instance_id) 2186 | if not tab: 2187 | return [] 2188 | functions = await cdp_function_executor.discover_global_functions(tab, context_id) 2189 | result = [ 2190 | { 2191 | "name": func.name, 2192 | "path": func.path, 2193 | "signature": func.signature, 2194 | "description": func.description 2195 | } 2196 | for func in functions 2197 | ] 2198 | 2199 | file_response = response_handler.handle_response( 2200 | result, 2201 | fallback_filename_prefix="global_functions", 2202 | metadata={ 2203 | "context_id": context_id, 2204 | "function_count": len(result), 2205 | "url": getattr(tab, 'url', 'unknown') 2206 | } 2207 | ) 2208 | 2209 | if isinstance(file_response, dict) and "file_path" in file_response: 2210 | return [{ 2211 | "name": "LARGE_RESPONSE_SAVED_TO_FILE", 2212 | "path": "file_storage", 2213 | "signature": "automatic_file_fallback", 2214 | "description": f"Response too large ({file_response['estimated_tokens']} tokens), saved to: {file_response['filename']}" 2215 | }] 2216 | 2217 | return file_response 2218 | 2219 | 2220 | @section_tool("cdp-functions") 2221 | async def discover_object_methods( 2222 | instance_id: str, 2223 | object_path: str 2224 | ) -> List[Dict[str, Any]]: 2225 | """ 2226 | Discover methods of a specific JavaScript object. 2227 | 2228 | Args: 2229 | instance_id (str): Browser instance ID. 2230 | object_path (str): Path to the object (e.g., 'document', 'window.localStorage'). 2231 | 2232 | Returns: 2233 | List[Dict[str, Any]]: List of discovered methods. 2234 | """ 2235 | tab = await browser_manager.get_tab(instance_id) 2236 | if not tab: 2237 | return [] 2238 | methods = await cdp_function_executor.discover_object_methods(tab, object_path) 2239 | methods_data = [ 2240 | { 2241 | "name": method.name, 2242 | "path": method.path, 2243 | "signature": method.signature, 2244 | "description": method.description 2245 | } 2246 | for method in methods 2247 | ] 2248 | 2249 | return await response_handler.handle_response( 2250 | methods_data, 2251 | f"object_methods_{object_path.replace('.', '_')}" 2252 | ) 2253 | 2254 | 2255 | @section_tool("cdp-functions") 2256 | async def call_javascript_function( 2257 | instance_id: str, 2258 | function_path: str, 2259 | args: List[Any] = None 2260 | ) -> Dict[str, Any]: 2261 | """ 2262 | Call a JavaScript function with arguments. 2263 | 2264 | Args: 2265 | instance_id (str): Browser instance ID. 2266 | function_path (str): Full path to the function (e.g., 'document.getElementById'). 2267 | args (List[Any], optional): List of arguments to pass to the function. 2268 | 2269 | Returns: 2270 | Dict[str, Any]: Function call result. 2271 | """ 2272 | tab = await browser_manager.get_tab(instance_id) 2273 | if not tab: 2274 | return {"success": False, "error": f"Instance not found: {instance_id}"} 2275 | return await cdp_function_executor.call_discovered_function(tab, function_path, args or []) 2276 | 2277 | 2278 | @section_tool("cdp-functions") 2279 | async def inspect_function_signature( 2280 | instance_id: str, 2281 | function_path: str 2282 | ) -> Dict[str, Any]: 2283 | """ 2284 | Inspect a JavaScript function's signature and details. 2285 | 2286 | Args: 2287 | instance_id (str): Browser instance ID. 2288 | function_path (str): Full path to the function. 2289 | 2290 | Returns: 2291 | Dict[str, Any]: Function signature and details. 2292 | """ 2293 | tab = await browser_manager.get_tab(instance_id) 2294 | if not tab: 2295 | return {"success": False, "error": f"Instance not found: {instance_id}"} 2296 | return await cdp_function_executor.inspect_function_signature(tab, function_path) 2297 | 2298 | 2299 | @section_tool("cdp-functions") 2300 | async def inject_and_execute_script( 2301 | instance_id: str, 2302 | script_code: str, 2303 | context_id: str = None 2304 | ) -> Dict[str, Any]: 2305 | """ 2306 | Inject and execute custom JavaScript code. 2307 | 2308 | Args: 2309 | instance_id (str): Browser instance ID. 2310 | script_code (str): JavaScript code to execute. 2311 | context_id (str, optional): Optional execution context ID. 2312 | 2313 | Returns: 2314 | Dict[str, Any]: Script execution result. 2315 | """ 2316 | tab = await browser_manager.get_tab(instance_id) 2317 | if not tab: 2318 | return {"success": False, "error": f"Instance not found: {instance_id}"} 2319 | return await cdp_function_executor.inject_and_execute_script(tab, script_code, context_id) 2320 | 2321 | 2322 | @section_tool("cdp-functions") 2323 | async def create_persistent_function( 2324 | instance_id: str, 2325 | function_name: str, 2326 | function_code: str 2327 | ) -> Dict[str, Any]: 2328 | """ 2329 | Create a persistent JavaScript function that survives page reloads. 2330 | 2331 | Args: 2332 | instance_id (str): Browser instance ID. 2333 | function_name (str): Name for the function. 2334 | function_code (str): JavaScript function code. 2335 | 2336 | Returns: 2337 | Dict[str, Any]: Function creation result. 2338 | """ 2339 | tab = await browser_manager.get_tab(instance_id) 2340 | if not tab: 2341 | return {"success": False, "error": f"Instance not found: {instance_id}"} 2342 | return await cdp_function_executor.create_persistent_function(tab, function_name, function_code, instance_id) 2343 | 2344 | 2345 | @section_tool("cdp-functions") 2346 | async def execute_function_sequence( 2347 | instance_id: str, 2348 | function_calls: List[Dict[str, Any]] 2349 | ) -> List[Dict[str, Any]]: 2350 | """ 2351 | Execute a sequence of JavaScript function calls. 2352 | 2353 | Args: 2354 | instance_id (str): Browser instance ID. 2355 | function_calls (List[Dict[str, Any]]): List of function calls, each with 'function_path', 'args', and optional 'context_id'. 2356 | 2357 | Returns: 2358 | List[Dict[str, Any]]: List of function call results. 2359 | """ 2360 | from cdp_function_executor import FunctionCall 2361 | tab = await browser_manager.get_tab(instance_id) 2362 | if not tab: 2363 | return [{"success": False, "error": f"Instance not found: {instance_id}"}] 2364 | calls = [] 2365 | for call_data in function_calls: 2366 | calls.append(FunctionCall( 2367 | function_path=call_data['function_path'], 2368 | args=call_data.get('args', []), 2369 | context_id=call_data.get('context_id') 2370 | )) 2371 | return await cdp_function_executor.execute_function_sequence(tab, calls) 2372 | 2373 | 2374 | @section_tool("cdp-functions") 2375 | async def create_python_binding( 2376 | instance_id: str, 2377 | binding_name: str, 2378 | python_code: str 2379 | ) -> Dict[str, Any]: 2380 | """ 2381 | Create a binding that allows JavaScript to call Python functions. 2382 | 2383 | Args: 2384 | instance_id (str): Browser instance ID. 2385 | binding_name (str): Name for the binding. 2386 | python_code (str): Python function code (as string). 2387 | 2388 | Returns: 2389 | Dict[str, Any]: Binding creation result. 2390 | """ 2391 | tab = await browser_manager.get_tab(instance_id) 2392 | if not tab: 2393 | return {"success": False, "error": f"Instance not found: {instance_id}"} 2394 | try: 2395 | exec_globals = {} 2396 | exec(python_code, exec_globals) 2397 | python_function = None 2398 | for name, obj in exec_globals.items(): 2399 | if callable(obj) and not name.startswith('_'): 2400 | python_function = obj 2401 | break 2402 | if not python_function: 2403 | return {"success": False, "error": "No function found in Python code"} 2404 | return await cdp_function_executor.create_python_binding(tab, binding_name, python_function) 2405 | except Exception as e: 2406 | return {"success": False, "error": f"Failed to create Python function: {str(e)}"} 2407 | 2408 | 2409 | @section_tool("cdp-functions") 2410 | async def execute_python_in_browser( 2411 | instance_id: str, 2412 | python_code: str 2413 | ) -> Dict[str, Any]: 2414 | """ 2415 | Execute Python code by translating it to JavaScript. 2416 | 2417 | Args: 2418 | instance_id (str): Browser instance ID. 2419 | python_code (str): Python code to translate and execute. 2420 | 2421 | Returns: 2422 | Dict[str, Any]: Execution result. 2423 | """ 2424 | tab = await browser_manager.get_tab(instance_id) 2425 | if not tab: 2426 | return {"success": False, "error": f"Instance not found: {instance_id}"} 2427 | return await cdp_function_executor.execute_python_in_browser(tab, python_code) 2428 | 2429 | 2430 | @section_tool("cdp-functions") 2431 | async def get_function_executor_info( 2432 | instance_id: str = None 2433 | ) -> Dict[str, Any]: 2434 | """ 2435 | Get information about the CDP function executor state. 2436 | 2437 | Args: 2438 | instance_id (str, optional): Optional browser instance ID for specific info. 2439 | 2440 | Returns: 2441 | Dict[str, Any]: Function executor state and capabilities. 2442 | """ 2443 | return await cdp_function_executor.get_function_executor_info(instance_id) 2444 | 2445 | 2446 | @section_tool("dynamic-hooks") 2447 | async def create_dynamic_hook( 2448 | name: str, 2449 | requirements: Dict[str, Any], 2450 | function_code: str, 2451 | instance_ids: Optional[List[str]] = None, 2452 | priority: int = 100 2453 | ) -> Dict[str, Any]: 2454 | """ 2455 | Create a new dynamic hook with AI-generated Python function. 2456 | 2457 | This is the new powerful hook system that allows AI to write custom Python functions 2458 | that process network requests in real-time with no pending state. 2459 | 2460 | Args: 2461 | name (str): Human-readable hook name 2462 | requirements (Dict[str, Any]): Matching criteria (url_pattern, method, resource_type, custom_condition) 2463 | function_code (str): Python function code that processes requests (must define process_request(request)) 2464 | instance_ids (Optional[List[str]]): Browser instances to apply hook to (all if None) 2465 | priority (int): Hook priority (lower = higher priority) 2466 | 2467 | Returns: 2468 | Dict[str, Any]: Hook creation result with hook_id 2469 | 2470 | Example function_code: 2471 | ```python 2472 | def process_request(request): 2473 | if "example.com" in request["url"]: 2474 | return HookAction(action="redirect", url="https://httpbin.org/get") 2475 | return HookAction(action="continue") 2476 | ``` 2477 | """ 2478 | return await dynamic_hook_ai.create_dynamic_hook( 2479 | name=name, 2480 | requirements=requirements, 2481 | function_code=function_code, 2482 | instance_ids=instance_ids, 2483 | priority=priority 2484 | ) 2485 | 2486 | 2487 | @section_tool("dynamic-hooks") 2488 | async def create_simple_dynamic_hook( 2489 | name: str, 2490 | url_pattern: str, 2491 | action: str, 2492 | target_url: Optional[str] = None, 2493 | custom_headers: Optional[Dict[str, str]] = None, 2494 | instance_ids: Optional[List[str]] = None 2495 | ) -> Dict[str, Any]: 2496 | """ 2497 | Create a simple dynamic hook using predefined templates (easier for AI). 2498 | 2499 | Args: 2500 | name (str): Hook name 2501 | url_pattern (str): URL pattern to match 2502 | action (str): Action type - 'block', 'redirect', 'add_headers', or 'log' 2503 | target_url (Optional[str]): Target URL for redirect action 2504 | custom_headers (Optional[Dict[str, str]]): Headers to add for add_headers action 2505 | instance_ids (Optional[List[str]]): Browser instances to apply hook to 2506 | 2507 | Returns: 2508 | Dict[str, Any]: Hook creation result 2509 | """ 2510 | return await dynamic_hook_ai.create_simple_hook( 2511 | name=name, 2512 | url_pattern=url_pattern, 2513 | action=action, 2514 | target_url=target_url, 2515 | custom_headers=custom_headers, 2516 | instance_ids=instance_ids 2517 | ) 2518 | 2519 | 2520 | @section_tool("dynamic-hooks") 2521 | async def list_dynamic_hooks(instance_id: Optional[str] = None) -> Dict[str, Any]: 2522 | """ 2523 | List all dynamic hooks. 2524 | 2525 | Args: 2526 | instance_id (Optional[str]): Optional filter by browser instance 2527 | 2528 | Returns: 2529 | Dict[str, Any]: List of hooks with details and statistics 2530 | """ 2531 | return await dynamic_hook_ai.list_dynamic_hooks(instance_id=instance_id) 2532 | 2533 | 2534 | @section_tool("dynamic-hooks") 2535 | async def get_dynamic_hook_details(hook_id: str) -> Dict[str, Any]: 2536 | """ 2537 | Get detailed information about a specific dynamic hook. 2538 | 2539 | Args: 2540 | hook_id (str): Hook identifier 2541 | 2542 | Returns: 2543 | Dict[str, Any]: Detailed hook information including function code 2544 | """ 2545 | return await dynamic_hook_ai.get_hook_details(hook_id=hook_id) 2546 | 2547 | 2548 | @section_tool("dynamic-hooks") 2549 | async def remove_dynamic_hook(hook_id: str) -> Dict[str, Any]: 2550 | """ 2551 | Remove a dynamic hook. 2552 | 2553 | Args: 2554 | hook_id (str): Hook identifier to remove 2555 | 2556 | Returns: 2557 | Dict[str, Any]: Removal status 2558 | """ 2559 | return await dynamic_hook_ai.remove_dynamic_hook(hook_id=hook_id) 2560 | 2561 | 2562 | @section_tool("dynamic-hooks") 2563 | def get_hook_documentation() -> Dict[str, Any]: 2564 | """ 2565 | Get comprehensive documentation for creating hook functions (AI learning). 2566 | 2567 | Returns: 2568 | Dict[str, Any]: Documentation of request object structure and HookAction types 2569 | """ 2570 | return dynamic_hook_ai.get_request_documentation() 2571 | 2572 | 2573 | @section_tool("dynamic-hooks") 2574 | def get_hook_examples() -> Dict[str, Any]: 2575 | """ 2576 | Get example hook functions for AI learning. 2577 | 2578 | Returns: 2579 | Dict[str, Any]: Collection of example hook functions with explanations 2580 | """ 2581 | return dynamic_hook_ai.get_hook_examples() 2582 | 2583 | 2584 | @section_tool("dynamic-hooks") 2585 | def get_hook_requirements_documentation() -> Dict[str, Any]: 2586 | """ 2587 | Get documentation on hook requirements and matching criteria. 2588 | 2589 | Returns: 2590 | Dict[str, Any]: Requirements documentation and best practices 2591 | """ 2592 | return dynamic_hook_ai.get_requirements_documentation() 2593 | 2594 | 2595 | @section_tool("dynamic-hooks") 2596 | def get_hook_common_patterns() -> Dict[str, Any]: 2597 | """ 2598 | Get common hook patterns and use cases. 2599 | 2600 | Returns: 2601 | Dict[str, Any]: Common patterns like ad blocking, API proxying, etc. 2602 | """ 2603 | return dynamic_hook_ai.get_common_patterns() 2604 | 2605 | 2606 | @section_tool("dynamic-hooks") 2607 | def validate_hook_function(function_code: str) -> Dict[str, Any]: 2608 | """ 2609 | Validate hook function code for common issues before creating. 2610 | 2611 | Args: 2612 | function_code (str): Python function code to validate 2613 | 2614 | Returns: 2615 | Dict[str, Any]: Validation results with issues and warnings 2616 | """ 2617 | return dynamic_hook_ai.validate_hook_function(function_code=function_code) 2618 | 2619 | 2620 | 2621 | if __name__ == "__main__": 2622 | import argparse 2623 | 2624 | parser = argparse.ArgumentParser(description="Stealth Browser MCP Server with 90 tools") 2625 | parser.add_argument("--transport", choices=["stdio", "http"], default="stdio", 2626 | help="Transport protocol to use") 2627 | parser.add_argument("--port", type=int, default=int(os.getenv("PORT", 8000)), 2628 | help="Port for HTTP transport") 2629 | parser.add_argument("--host", default="0.0.0.0", 2630 | help="Host for HTTP transport") 2631 | 2632 | parser.add_argument("--disable-browser-management", action="store_true", 2633 | help="Disable browser management tools (spawn, navigate, close, etc.)") 2634 | parser.add_argument("--disable-element-interaction", action="store_true", 2635 | help="Disable element interaction tools (click, type, scroll, etc.)") 2636 | parser.add_argument("--disable-element-extraction", action="store_true", 2637 | help="Disable element extraction tools (styles, structure, events, etc.)") 2638 | parser.add_argument("--disable-file-extraction", action="store_true", 2639 | help="Disable file-based extraction tools") 2640 | parser.add_argument("--disable-network-debugging", action="store_true", 2641 | help="Disable network debugging and interception tools") 2642 | parser.add_argument("--disable-cdp-functions", action="store_true", 2643 | help="Disable CDP function execution tools") 2644 | parser.add_argument("--disable-progressive-cloning", action="store_true", 2645 | help="Disable progressive element cloning tools") 2646 | parser.add_argument("--disable-cookies-storage", action="store_true", 2647 | help="Disable cookie and storage management tools") 2648 | parser.add_argument("--disable-tabs", action="store_true", 2649 | help="Disable tab management tools") 2650 | parser.add_argument("--disable-debugging", action="store_true", 2651 | help="Disable debug and system tools") 2652 | parser.add_argument("--disable-dynamic-hooks", action="store_true", 2653 | help="Disable dynamic network hook system") 2654 | 2655 | parser.add_argument("--minimal", action="store_true", 2656 | help="Enable only core browser management and element interaction (disable everything else)") 2657 | parser.add_argument("--list-sections", action="store_true", 2658 | help="List all available tool sections and exit") 2659 | 2660 | args = parser.parse_args() 2661 | 2662 | if args.list_sections: 2663 | print("Available tool sections:") 2664 | print(" browser-management: Core browser operations (11 tools)") 2665 | print(" element-interaction: Page interaction and element manipulation (8 tools)") 2666 | print(" element-extraction: Element cloning and extraction (10 tools)") 2667 | print(" file-extraction: File-based extraction tools (9 tools)") 2668 | print(" network-debugging: Network monitoring and interception (10 tools)") 2669 | print(" cdp-functions: Chrome DevTools Protocol function execution (15 tools)") 2670 | print(" progressive-cloning: Advanced element cloning system (10 tools)") 2671 | print(" cookies-storage: Cookie and storage management (3 tools)") 2672 | print(" tabs: Tab management (5 tools)") 2673 | print(" debugging: Debug and system tools (6 tools)") 2674 | print(" dynamic-hooks: AI-powered network hook system (12 tools)") 2675 | print("\nUse --disable-<section-name> to disable specific sections") 2676 | print("Use --minimal to enable only core functionality") 2677 | sys.exit(0) 2678 | 2679 | if args.minimal: 2680 | DISABLED_SECTIONS.update([ 2681 | "element-extraction", "file-extraction", "network-debugging", 2682 | "cdp-functions", "progressive-cloning", "cookies-storage", 2683 | "tabs", "debugging", "dynamic-hooks" 2684 | ]) 2685 | 2686 | if args.disable_browser_management: 2687 | DISABLED_SECTIONS.add("browser-management") 2688 | if args.disable_element_interaction: 2689 | DISABLED_SECTIONS.add("element-interaction") 2690 | if args.disable_element_extraction: 2691 | DISABLED_SECTIONS.add("element-extraction") 2692 | if args.disable_file_extraction: 2693 | DISABLED_SECTIONS.add("file-extraction") 2694 | if args.disable_network_debugging: 2695 | DISABLED_SECTIONS.add("network-debugging") 2696 | if args.disable_cdp_functions: 2697 | DISABLED_SECTIONS.add("cdp-functions") 2698 | if args.disable_progressive_cloning: 2699 | DISABLED_SECTIONS.add("progressive-cloning") 2700 | if args.disable_cookies_storage: 2701 | DISABLED_SECTIONS.add("cookies-storage") 2702 | if args.disable_tabs: 2703 | DISABLED_SECTIONS.add("tabs") 2704 | if args.disable_debugging: 2705 | DISABLED_SECTIONS.add("debugging") 2706 | if args.disable_dynamic_hooks: 2707 | DISABLED_SECTIONS.add("dynamic-hooks") 2708 | 2709 | if DISABLED_SECTIONS: 2710 | print(f"Disabled tool sections: {', '.join(sorted(DISABLED_SECTIONS))}") 2711 | 2712 | if args.transport == "http": 2713 | mcp.run(transport="http", host=args.host, port=args.port) 2714 | else: 2715 | mcp.run(transport="stdio") ```