# Directory Structure ``` ├── changelog.md ├── LICENSE ├── pyproject.toml ├── README.md └── src └── mcp_web_browser └── server.py ``` # Files -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # MCP Web Browser Server 2 | 3 | An advanced web browsing server for the Model Context Protocol (MCP) powered by Playwright, enabling headless browser interactions through a flexible, secure API. 4 | 5 | <a href="https://glama.ai/mcp/servers/lwqlaw6k6d"><img width="380" height="200" src="https://glama.ai/mcp/servers/lwqlaw6k6d/badge" alt="Web Browser Server MCP server" /></a> 6 | 7 | ## 🌐 Features 8 | 9 | - **Headless Web Browsing**: Navigate to any website with SSL certificate validation bypass 10 | - **Full Page Content Extraction**: Retrieve complete HTML content, including dynamically loaded JavaScript 11 | - **Multi-Tab Support**: Create, manage, and switch between multiple browser tabs 12 | - **Advanced Web Interaction Tools**: 13 | - Extract text content 14 | - Click page elements 15 | - Input text into form fields 16 | - Capture screenshots 17 | - Extract page links with filtering capabilities 18 | - Scroll pages in any direction 19 | - Execute JavaScript on pages 20 | - Refresh pages 21 | - Wait for navigation to complete 22 | - **Resource Management**: Automatic cleanup of unused resources after inactivity 23 | - **Enhanced Page Information**: Get detailed metadata about the current page 24 | 25 | ## 🚀 Quick Start 26 | 27 | ### Prerequisites 28 | 29 | - Python 3.10+ 30 | - MCP SDK 31 | - Playwright 32 | 33 | ### Installation 34 | 35 | ```bash 36 | # Install MCP and Playwright 37 | pip install mcp playwright 38 | 39 | # Install browser dependencies 40 | playwright install 41 | ``` 42 | 43 | ### Configuration for Claude Desktop 44 | 45 | Add to your `claude_desktop_config.json`: 46 | 47 | ```json 48 | { 49 | "mcpServers": { 50 | "web-browser": { 51 | "command": "python", 52 | "args": [ 53 | "/path/to/your/server.py" 54 | ] 55 | } 56 | } 57 | } 58 | ``` 59 | 60 | ## 💡 Usage Examples 61 | 62 | ### Basic Web Navigation 63 | 64 | ```python 65 | # Browse to a website 66 | page_content = browse_to("https://example.com") 67 | 68 | # Extract page text 69 | text_content = extract_text_content() 70 | 71 | # Extract text from a specific element 72 | title_text = extract_text_content("h1.title") 73 | ``` 74 | 75 | ### Web Interaction 76 | 77 | ```python 78 | # Navigate to a page 79 | browse_to("https://example.com/login") 80 | 81 | # Input text into a form 82 | input_text("#username", "your_username") 83 | input_text("#password", "your_password") 84 | 85 | # Click a login button 86 | click_element("#login-button") 87 | ``` 88 | 89 | ### Screenshot Capture 90 | 91 | ```python 92 | # Capture full page screenshot 93 | full_page_screenshot = get_page_screenshots(full_page=True) 94 | 95 | # Capture specific element screenshot 96 | element_screenshot = get_page_screenshots(selector="#main-content") 97 | ``` 98 | 99 | ### Link Extraction 100 | 101 | ```python 102 | # Get all links on the page 103 | page_links = get_page_links() 104 | 105 | # Get links matching a pattern 106 | filtered_links = get_page_links(filter_pattern="contact") 107 | ``` 108 | 109 | ### Multi-Tab Browsing 110 | 111 | ```python 112 | # Create a new tab 113 | tab_id = create_new_tab("https://example.com") 114 | 115 | # Create another tab 116 | another_tab_id = create_new_tab("https://example.org") 117 | 118 | # List all open tabs 119 | tabs = list_tabs() 120 | 121 | # Switch between tabs 122 | switch_tab(tab_id) 123 | 124 | # Close a tab 125 | close_tab(another_tab_id) 126 | ``` 127 | 128 | ### Advanced Interactions 129 | 130 | ```python 131 | # Scroll the page 132 | scroll_page(direction="down", amount="page") 133 | 134 | # Execute JavaScript on the page 135 | result = execute_javascript("return document.title") 136 | 137 | # Get detailed page information 138 | page_info = get_page_info() 139 | 140 | # Refresh the current page 141 | refresh_page() 142 | 143 | # Wait for navigation to complete 144 | wait_for_navigation(timeout_ms=5000) 145 | ``` 146 | 147 | ## 🛡️ Security Features 148 | 149 | - SSL certificate validation bypass 150 | - Secure browser context management 151 | - Custom user-agent configuration 152 | - Error handling and comprehensive logging 153 | - Configurable timeout settings 154 | - CSP bypass control 155 | - Protection against cookie stealing 156 | 157 | ## 🔧 Troubleshooting 158 | 159 | ### Common Issues 160 | 161 | - **SSL Certificate Errors**: Automatically bypassed 162 | - **Slow Page Load**: Adjust timeout in `browse_to()` method 163 | - **Element Not Found**: Verify selectors carefully 164 | - **Browser Resource Usage**: Auto-cleanup after inactivity period 165 | 166 | ### Logging 167 | 168 | All significant events are logged with detailed information for easy debugging. 169 | 170 | ## 📋 Tool Parameters 171 | 172 | ### `browse_to(url: str, context: Optional[Any] = None)` 173 | - `url`: Website to navigate to 174 | - `context`: Optional context object (currently unused) 175 | 176 | ### `extract_text_content(selector: Optional[str] = None, context: Optional[Any] = None)` 177 | - `selector`: Optional CSS selector to extract specific content 178 | - `context`: Optional context object (currently unused) 179 | 180 | ### `click_element(selector: str, context: Optional[Any] = None)` 181 | - `selector`: CSS selector of the element to click 182 | - `context`: Optional context object (currently unused) 183 | 184 | ### `get_page_screenshots(full_page: bool = False, selector: Optional[str] = None, context: Optional[Any] = None)` 185 | - `full_page`: Capture entire page screenshot 186 | - `selector`: Optional element to screenshot 187 | - `context`: Optional context object (currently unused) 188 | 189 | ### `get_page_links(filter_pattern: Optional[str] = None, context: Optional[Any] = None)` 190 | - `filter_pattern`: Optional text pattern to filter links 191 | - `context`: Optional context object (currently unused) 192 | 193 | ### `input_text(selector: str, text: str, context: Optional[Any] = None)` 194 | - `selector`: CSS selector of input element 195 | - `text`: Text to input 196 | - `context`: Optional context object (currently unused) 197 | 198 | ### `create_new_tab(url: Optional[str] = None, context: Optional[Any] = None)` 199 | - `url`: Optional URL to navigate to in the new tab 200 | - `context`: Optional context object (currently unused) 201 | 202 | ### `switch_tab(tab_id: str, context: Optional[Any] = None)` 203 | - `tab_id`: ID of the tab to switch to 204 | - `context`: Optional context object (currently unused) 205 | 206 | ### `list_tabs(context: Optional[Any] = None)` 207 | - `context`: Optional context object (currently unused) 208 | 209 | ### `close_tab(tab_id: Optional[str] = None, context: Optional[Any] = None)` 210 | - `tab_id`: Optional ID of the tab to close (defaults to current tab) 211 | - `context`: Optional context object (currently unused) 212 | 213 | ### `refresh_page(context: Optional[Any] = None)` 214 | - `context`: Optional context object (currently unused) 215 | 216 | ### `get_page_info(context: Optional[Any] = None)` 217 | - `context`: Optional context object (currently unused) 218 | 219 | ### `scroll_page(direction: str = "down", amount: str = "page", context: Optional[Any] = None)` 220 | - `direction`: Direction to scroll ('up', 'down', 'left', 'right') 221 | - `amount`: Amount to scroll ('page', 'half', or a number) 222 | - `context`: Optional context object (currently unused) 223 | 224 | ### `wait_for_navigation(timeout_ms: int = 10000, context: Optional[Any] = None)` 225 | - `timeout_ms`: Maximum time to wait in milliseconds 226 | - `context`: Optional context object (currently unused) 227 | 228 | ### `execute_javascript(script: str, context: Optional[Any] = None)` 229 | - `script`: JavaScript code to execute 230 | - `context`: Optional context object (currently unused) 231 | 232 | ## 🤝 Contributing 233 | 234 | Contributions are welcome! Please feel free to submit a Pull Request. 235 | 236 | ### Development Setup 237 | 238 | ```bash 239 | # Clone the repository 240 | git clone https://github.com/random-robbie/mcp-web-browser.git 241 | 242 | # Create virtual environment 243 | python -m venv venv 244 | source venv/bin/activate # On Windows use `venv\Scripts\activate` 245 | 246 | # Install dependencies 247 | pip install -e .[dev] 248 | ``` 249 | 250 | ## 📄 License 251 | 252 | MIT License 253 | 254 | ## 🔗 Related Projects 255 | 256 | - [Model Context Protocol](https://modelcontextprotocol.io) 257 | - [Playwright](https://playwright.dev) 258 | - [Claude Desktop](https://claude.ai/desktop) 259 | 260 | ## 💬 Support 261 | 262 | For issues and questions, please [open an issue](https://github.com/random-robbie/mcp-web-browser/issues) on GitHub. 263 | ``` -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- ```toml 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "mcp-web-browser" 7 | version = "0.1.0" 8 | description = "MCP Web Browser Server using Playwright" 9 | readme = "README.md" 10 | requires-python = ">=3.10" 11 | dependencies = [ 12 | "mcp[cli]", 13 | "playwright", 14 | ] 15 | 16 | [project.optional-dependencies] 17 | dev = [ 18 | "pytest", 19 | "ruff", 20 | ] 21 | 22 | [tool.hatch.build.targets.wheel] 23 | packages = ["src/mcp_web_browser"] 24 | 25 | [tool.hatch.envs.default] 26 | dependencies = [ 27 | "pytest", 28 | "ruff", 29 | ] 30 | 31 | [tool.ruff] 32 | line-length = 100 33 | target-version = "py310" 34 | 35 | [tool.ruff.lint] 36 | select = ["E", "F", "W", "I", "N", "UP", "ASYNC"] 37 | ignore = ["E501"] 38 | 39 | [project.scripts] 40 | mcp-web-browser = "mcp_web_browser.server:main" 41 | ``` -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- ```markdown 1 | Security Enhancements 2 | 3 | Proper Error Handling and Logging: Added comprehensive logging with formatted output and proper error tracing 4 | URL Sanitization: Automatically adds HTTPS prefix to URLs when needed 5 | Content Security Policy Bypass Control: Added option to bypass CSP for better testing capabilities 6 | Custom User-Agent: Configurable user-agent string 7 | Security Headers: Added security-related HTTP headers to requests 8 | JavaScript Execution Restrictions: Added validation to prevent cookie stealing via JavaScript 9 | 10 | Performance Improvements 11 | 12 | Browser Inactivity Timeout: Automatically closes browser after inactivity to free resources 13 | Optimized Page Loading: Changed from 'networkidle' to 'domcontentloaded' for faster page loading 14 | Resource Cleanup: Improved cleanup processes to ensure all resources are properly released 15 | Retry Logic: Added retry mechanisms for page creation and element clicking 16 | Efficient Screenshot Capture: Added JPEG compression for faster screenshot processing 17 | 18 | Functional Additions 19 | 20 | Multi-Tab Support: Added ability to create, switch between, and manage multiple tabs 21 | 22 | create_new_tab: Opens a new browser tab 23 | switch_tab: Switches between open tabs 24 | list_tabs: Shows all open tabs and their information 25 | close_tab: Closes a specific tab 26 | 27 | 28 | Advanced Page Interaction: 29 | 30 | refresh_page: Refreshes the current page 31 | get_page_info: Provides detailed page information including metadata 32 | scroll_page: Controls page scrolling in all directions 33 | wait_for_navigation: Waits for page navigation to complete 34 | execute_javascript: Safely executes JavaScript on the page 35 | 36 | 37 | Enhanced Link Extraction: 38 | 39 | Added filtering capabilities for links 40 | Returns more metadata about links (title, text, etc.) 41 | 42 | 43 | Improved Element Interaction: 44 | 45 | Better visibility checking for elements 46 | Automatic scrolling to ensure elements are in view 47 | More reliable clicking with retry logic 48 | 49 | 50 | 51 | Code Structure and Reliability 52 | 53 | Better State Management: Added global state tracking for browser, pages, and tabs 54 | Background Monitoring: Added inactivity monitor to clean up unused resources 55 | Detailed Logging: Comprehensive logging throughout the system 56 | Dynamic Playwright Import: Better error handling for Playwright import 57 | Configurability: Added more configuration options for timeouts, viewport size, etc. 58 | 59 | These improvements make the web browser MCP server more robust, secure, and feature-rich for your pentesting work, while ensuring resources are properly managed. 60 | ``` -------------------------------------------------------------------------------- /src/mcp_web_browser/server.py: -------------------------------------------------------------------------------- ```python 1 | from typing import Optional, Union, Any 2 | import asyncio 3 | import base64 4 | import sys 5 | from mcp.server import FastMCP 6 | 7 | # Create an MCP server for web browsing 8 | mcp = FastMCP("Web Browser") 9 | 10 | # Global browser and page management 11 | _browser = None 12 | _browser_context = None 13 | _current_page = None 14 | _playwright_instance = None 15 | 16 | # Dynamic import of Playwright to avoid early import errors 17 | def _import_playwright(): 18 | from playwright.async_api import async_playwright 19 | return async_playwright 20 | 21 | async def _ensure_browser(): 22 | """Ensure a browser instance is available with SSL validation disabled""" 23 | global _browser, _browser_context, _playwright_instance 24 | 25 | if _browser is None: 26 | playwright_module = _import_playwright() 27 | _playwright_instance = await playwright_module().start() 28 | _browser = await _playwright_instance.chromium.launch() 29 | 30 | # Create a browser context that ignores HTTPS errors 31 | _browser_context = await _browser.new_context( 32 | ignore_https_errors=True, # Ignore SSL certificate errors 33 | ) 34 | return _browser, _browser_context 35 | 36 | async def _close_current_page(): 37 | """Close the current page if it exists""" 38 | global _current_page 39 | if _current_page: 40 | try: 41 | await _current_page.close() 42 | except Exception: 43 | pass 44 | _current_page = None 45 | 46 | async def _safe_cleanup(): 47 | """Safely clean up browser resources""" 48 | global _browser, _current_page, _browser_context, _playwright_instance 49 | 50 | try: 51 | if _current_page: 52 | try: 53 | await _current_page.close() 54 | except Exception: 55 | pass 56 | 57 | if _browser_context: 58 | try: 59 | await _browser_context.close() 60 | except Exception: 61 | pass 62 | 63 | if _browser: 64 | try: 65 | await _browser.close() 66 | except Exception: 67 | pass 68 | 69 | if _playwright_instance: 70 | try: 71 | await _playwright_instance.stop() 72 | except Exception: 73 | pass 74 | except Exception as e: 75 | # Log the error, but don't re-raise 76 | print(f"Error during cleanup: {e}", file=sys.stderr) 77 | finally: 78 | # Reset global variables 79 | _browser = None 80 | _browser_context = None 81 | _current_page = None 82 | _playwright_instance = None 83 | 84 | @mcp.tool() 85 | async def browse_to(url: str, context: Optional[Any] = None) -> str: 86 | """ 87 | Navigate to a specific URL and return the page's HTML content. 88 | 89 | Args: 90 | url: The full URL to navigate to 91 | context: Optional context object for logging (ignored) 92 | 93 | Returns: 94 | The full HTML content of the page 95 | """ 96 | global _current_page, _browser, _browser_context 97 | 98 | # Ensure browser is launched with SSL validation disabled 99 | _, browser_context = await _ensure_browser() 100 | 101 | # Close any existing page 102 | await _close_current_page() 103 | 104 | # Optional logging, but do nothing with context 105 | print(f"Navigating to {url}", file=sys.stderr) 106 | 107 | try: 108 | # Create a new page and navigate 109 | _current_page = await browser_context.new_page() 110 | 111 | # Additional options to handle various SSL/security issues 112 | await _current_page.goto(url, 113 | wait_until='networkidle', 114 | timeout=30000, # 30 seconds timeout 115 | ) 116 | 117 | # Get full page content including dynamically loaded JavaScript 118 | page_content = await _current_page.content() 119 | 120 | # Optional: extract additional metadata 121 | try: 122 | title = await _current_page.title() 123 | print(f"Page title: {title}", file=sys.stderr) 124 | except Exception: 125 | pass 126 | 127 | return page_content 128 | 129 | except Exception as e: 130 | print(f"Error navigating to {url}: {e}", file=sys.stderr) 131 | raise 132 | 133 | @mcp.tool() 134 | async def extract_text_content( 135 | selector: Optional[str] = None, 136 | context: Optional[Any] = None 137 | ) -> str: 138 | """ 139 | Extract text content from the current page, optionally using a CSS selector. 140 | 141 | Args: 142 | selector: Optional CSS selector to target specific elements 143 | context: Optional context object for logging (ignored) 144 | 145 | Returns: 146 | Extracted text content 147 | """ 148 | global _current_page 149 | 150 | if not _current_page: 151 | raise ValueError("No page is currently loaded. Use browse_to first.") 152 | 153 | try: 154 | if selector: 155 | # If selector is provided, extract text from matching elements 156 | elements = await _current_page.query_selector_all(selector) 157 | text_content = "\n".join([await el.inner_text() for el in elements]) 158 | print(f"Extracted text from selector: {selector}", file=sys.stderr) 159 | else: 160 | # If no selector, extract all visible text from the page 161 | text_content = await _current_page.inner_text('body') 162 | 163 | return text_content 164 | 165 | except Exception as e: 166 | print(f"Error extracting text: {e}", file=sys.stderr) 167 | raise ValueError(f"Error extracting text: {e}") 168 | 169 | @mcp.tool() 170 | async def click_element( 171 | selector: str, 172 | context: Optional[Any] = None 173 | ) -> str: 174 | """ 175 | Click an element on the current page. 176 | 177 | Args: 178 | selector: CSS selector for the element to click 179 | context: Optional context object for logging (ignored) 180 | 181 | Returns: 182 | Confirmation message or error details 183 | """ 184 | global _current_page 185 | 186 | if not _current_page: 187 | raise ValueError("No page is currently loaded. Use browse_to first.") 188 | 189 | try: 190 | element = await _current_page.query_selector(selector) 191 | if not element: 192 | raise ValueError(f"No element found with selector: {selector}") 193 | 194 | await element.click() 195 | print(f"Clicked element: {selector}", file=sys.stderr) 196 | 197 | return f"Successfully clicked element: {selector}" 198 | 199 | except Exception as e: 200 | print(f"Error clicking element: {e}", file=sys.stderr) 201 | raise ValueError(f"Error clicking element: {e}") 202 | 203 | @mcp.tool() 204 | async def get_page_screenshots( 205 | full_page: bool = False, 206 | selector: Optional[str] = None, 207 | context: Optional[Any] = None 208 | ) -> str: 209 | """ 210 | Capture screenshot of the current page. 211 | 212 | Args: 213 | full_page: Whether to capture the entire page or just the viewport 214 | selector: Optional CSS selector to screenshot a specific element 215 | context: Optional context object for logging (ignored) 216 | 217 | Returns: 218 | Base64 encoded screenshot image 219 | """ 220 | global _current_page 221 | 222 | if not _current_page: 223 | raise ValueError("No page is currently loaded. Use browse_to first.") 224 | 225 | try: 226 | if selector: 227 | element = await _current_page.query_selector(selector) 228 | if not element: 229 | raise ValueError(f"No element found with selector: {selector}") 230 | screenshot_bytes = await element.screenshot() 231 | else: 232 | screenshot_bytes = await _current_page.screenshot(full_page=full_page) 233 | 234 | # Convert to base64 for easy transmission 235 | screenshot_base64 = base64.b64encode(screenshot_bytes).decode('utf-8') 236 | 237 | print(f"Screenshot captured: {'full page' if full_page else 'viewport'}", file=sys.stderr) 238 | 239 | return screenshot_base64 240 | 241 | except Exception as e: 242 | print(f"Error capturing screenshot: {e}", file=sys.stderr) 243 | raise ValueError(f"Error capturing screenshot: {e}") 244 | 245 | @mcp.tool() 246 | async def get_page_links(context: Optional[Any] = None) -> list[str]: 247 | """ 248 | Extract all links from the current page. 249 | 250 | Args: 251 | context: Optional context object for logging (ignored) 252 | 253 | Returns: 254 | List of links found on the page 255 | """ 256 | global _current_page 257 | 258 | if not _current_page: 259 | raise ValueError("No page is currently loaded. Use browse_to first.") 260 | 261 | try: 262 | # Use JavaScript to extract all links 263 | links = await _current_page.evaluate(""" 264 | () => { 265 | const links = document.querySelectorAll('a'); 266 | return Array.from(links).map(link => link.href); 267 | } 268 | """) 269 | 270 | print(f"Extracted {len(links)} links from the page", file=sys.stderr) 271 | 272 | return links 273 | 274 | except Exception as e: 275 | print(f"Error extracting links: {e}", file=sys.stderr) 276 | raise ValueError(f"Error extracting links: {e}") 277 | 278 | @mcp.tool() 279 | async def input_text( 280 | selector: str, 281 | text: str, 282 | context: Optional[Any] = None 283 | ) -> str: 284 | """ 285 | Input text into a specific element on the page. 286 | 287 | Args: 288 | selector: CSS selector for the input element 289 | text: Text to input 290 | context: Optional context object for logging (ignored) 291 | 292 | Returns: 293 | Confirmation message 294 | """ 295 | global _current_page 296 | 297 | if not _current_page: 298 | raise ValueError("No page is currently loaded. Use browse_to first.") 299 | 300 | try: 301 | element = await _current_page.query_selector(selector) 302 | if not element: 303 | raise ValueError(f"No element found with selector: {selector}") 304 | 305 | await element.fill(text) 306 | 307 | print(f"Input text into element: {selector}", file=sys.stderr) 308 | 309 | return f"Successfully input text into element: {selector}" 310 | 311 | except Exception as e: 312 | print(f"Error inputting text: {e}", file=sys.stderr) 313 | raise ValueError(f"Error inputting text: {e}") 314 | 315 | def main(): 316 | try: 317 | mcp.run() 318 | except Exception as e: 319 | print(f"Error running MCP server: {e}", file=sys.stderr) 320 | finally: 321 | # Use a separate event loop to ensure cleanup 322 | try: 323 | loop = asyncio.new_event_loop() 324 | asyncio.set_event_loop(loop) 325 | loop.run_until_complete(_safe_cleanup()) 326 | loop.close() 327 | except Exception as cleanup_error: 328 | print(f"Cleanup error: {cleanup_error}", file=sys.stderr) 329 | 330 | if __name__ == "__main__": 331 | main() 332 | ```