Skip to main content
Glama
browser.py10.8 kB
""" Browser Management Module This module handles the lifecycle and configuration of Playwright browsers for the MCP server. It provides a centralized interface for browser operations including startup, context management, and cleanup. The BrowserManager class encapsulates all browser-related functionality and provides a clean interface for other modules to interact with web pages. Key Features: - Configurable browser options (headless, viewport, user agent) - Context isolation for security - Automatic cleanup and resource management - Support for multiple browser types (Chromium, Firefox, WebKit) """ import asyncio import logging import os from typing import Optional, Dict, Any, List from pathlib import Path from playwright.async_api import async_playwright, Browser, BrowserContext, Page, Playwright logger = logging.getLogger(__name__) class BrowserManager: """ Manages Playwright browser instances and contexts. This class handles the lifecycle of browser processes, providing a high-level interface for browser operations while managing resources efficiently. Attributes: playwright: Playwright instance browser: Current browser instance context: Current browser context page: Current active page config: Browser configuration options """ def __init__(self, config: Optional[Dict[str, Any]] = None): """ Initialize the browser manager with optional configuration. Args: config: Optional configuration dictionary with browser settings """ self.playwright: Optional[Playwright] = None self.browser: Optional[Browser] = None self.context: Optional[BrowserContext] = None self.page: Optional[Page] = None # Default configuration self.config = { "headless": os.getenv("BROWSER_HEADLESS", "true").lower() == "true", "browser_type": os.getenv("BROWSER_TYPE", "chromium"), "viewport": {"width": 1280, "height": 720}, "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "timeout": int(os.getenv("BROWSER_TIMEOUT", "30000")), "downloads_path": str(Path.home() / "Downloads"), "ignore_https_errors": True } # Override with provided config if config: self.config.update(config) logger.info(f"Browser manager initialized with config: {self.config}") async def start(self) -> None: """ Start the browser with configured options. Initializes Playwright, launches browser, creates context and page. Must be called before using any browser functionality. Raises: RuntimeError: If browser fails to start or is already running """ if self.playwright is not None: raise RuntimeError("Browser is already running") try: logger.info("Starting Playwright browser...") self.playwright = await async_playwright().start() # Get browser type browser_type = getattr(self.playwright, self.config["browser_type"]) # Launch browser with options launch_options = { "headless": self.config["headless"], "args": [ "--no-sandbox", "--disable-blink-features=AutomationControlled", "--disable-web-security" ] } self.browser = await browser_type.launch(**launch_options) # Create browser context context_options = { "viewport": self.config["viewport"], "user_agent": self.config["user_agent"], "ignore_https_errors": self.config["ignore_https_errors"] } self.context = await self.browser.new_context(**context_options) # Set default timeout self.context.set_default_timeout(self.config["timeout"]) # Create initial page self.page = await self.context.new_page() logger.info(f"Browser started successfully: {self.config['browser_type']}") except Exception as e: logger.error(f"Failed to start browser: {str(e)}") await self.cleanup() raise RuntimeError(f"Browser startup failed: {str(e)}") async def get_current_page(self) -> Page: """ Get the current active page. Returns: Current Page instance Raises: RuntimeError: If browser is not started or no page available """ if not self.page: if not self.context: raise RuntimeError("Browser not started. Call start() first.") self.page = await self.context.new_page() return self.page async def new_page(self) -> Page: """ Create a new page in the current context. Returns: New Page instance Raises: RuntimeError: If browser is not started """ if not self.context: raise RuntimeError("Browser not started. Call start() first.") new_page = await self.context.new_page() self.page = new_page # Set as current page logger.info("Created new page") return new_page async def get_all_pages(self) -> List[Page]: """ Get all open pages in the current context. Returns: List of all Page instances Raises: RuntimeError: If browser is not started """ if not self.context: raise RuntimeError("Browser not started. Call start() first.") return self.context.pages async def close_page(self, page: Optional[Page] = None) -> None: """ Close a specific page or the current page. Args: page: Page to close. If None, closes current page. """ target_page = page or self.page if target_page: await target_page.close() logger.info("Page closed") # If we closed the current page, set to another or create new if target_page == self.page: pages = await self.get_all_pages() if pages: self.page = pages[0] else: self.page = None async def set_viewport(self, width: int, height: int) -> None: """ Set viewport size for the current page. Args: width: Viewport width in pixels height: Viewport height in pixels """ if not self.page: raise RuntimeError("No active page available") await self.page.set_viewport_size({"width": width, "height": height}) self.config["viewport"] = {"width": width, "height": height} logger.info(f"Viewport set to {width}x{height}") async def set_user_agent(self, user_agent: str) -> None: """ Set user agent for the current context. Args: user_agent: User agent string to set """ if not self.context: raise RuntimeError("Browser not started") # Note: Playwright requires creating new context to change user agent # This is a limitation of the underlying browser engines logger.warning("User agent change requires restart for full effect") self.config["user_agent"] = user_agent async def enable_request_interception(self) -> None: """ Enable request interception for the current page. Allows monitoring and modifying network requests. """ if not self.page: raise RuntimeError("No active page available") await self.page.route("**/*", self._handle_route) logger.info("Request interception enabled") async def _handle_route(self, route, request) -> None: """ Default route handler for intercepted requests. Args: route: Playwright route object request: Playwright request object """ # Default behavior: continue with request await route.continue_() async def get_browser_info(self) -> Dict[str, Any]: """ Get information about the current browser instance. Returns: Dictionary containing browser information """ if not self.browser: return {"status": "not_started"} contexts = len(self.browser.contexts) pages = len(await self.get_all_pages()) if self.context else 0 return { "status": "running", "browser_type": self.config["browser_type"], "headless": self.config["headless"], "contexts": contexts, "pages": pages, "current_url": await self.page.url() if self.page else None, "viewport": self.config["viewport"] } async def cleanup(self) -> None: """ Clean up all browser resources. Closes all pages, contexts, and the browser instance. Should be called when shutting down the server. """ logger.info("Cleaning up browser resources...") try: if self.page: await self.page.close() self.page = None if self.context: await self.context.close() self.context = None if self.browser: await self.browser.close() self.browser = None if self.playwright: await self.playwright.stop() self.playwright = None logger.info("Browser cleanup complete") except Exception as e: logger.error(f"Error during browser cleanup: {str(e)}") async def restart(self) -> None: """ Restart the browser with current configuration. Useful for recovering from errors or applying new settings. """ logger.info("Restarting browser...") await self.cleanup() await self.start() logger.info("Browser restart complete")

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sac916/claude-browser-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server