#
tokens: 4562/50000 4/4 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── changelog.md
├── LICENSE
├── pyproject.toml
├── README.md
└── src
    └── mcp_web_browser
        └── server.py
```

# Files

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# MCP Web Browser Server

An advanced web browsing server for the Model Context Protocol (MCP) powered by Playwright, enabling headless browser interactions through a flexible, secure API.

<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>

## 🌐 Features

- **Headless Web Browsing**: Navigate to any website with SSL certificate validation bypass
- **Full Page Content Extraction**: Retrieve complete HTML content, including dynamically loaded JavaScript
- **Multi-Tab Support**: Create, manage, and switch between multiple browser tabs
- **Advanced Web Interaction Tools**:
  - Extract text content
  - Click page elements
  - Input text into form fields
  - Capture screenshots
  - Extract page links with filtering capabilities
  - Scroll pages in any direction
  - Execute JavaScript on pages
  - Refresh pages
  - Wait for navigation to complete
- **Resource Management**: Automatic cleanup of unused resources after inactivity
- **Enhanced Page Information**: Get detailed metadata about the current page

## 🚀 Quick Start

### Prerequisites

- Python 3.10+
- MCP SDK
- Playwright

### Installation

```bash
# Install MCP and Playwright
pip install mcp playwright

# Install browser dependencies
playwright install
```

### Configuration for Claude Desktop

Add to your `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "web-browser": {
      "command": "python",
      "args": [
        "/path/to/your/server.py"
      ]
    }
  }
}
```

## 💡 Usage Examples

### Basic Web Navigation

```python
# Browse to a website
page_content = browse_to("https://example.com")

# Extract page text
text_content = extract_text_content()

# Extract text from a specific element
title_text = extract_text_content("h1.title")
```

### Web Interaction

```python
# Navigate to a page
browse_to("https://example.com/login")

# Input text into a form
input_text("#username", "your_username")
input_text("#password", "your_password")

# Click a login button
click_element("#login-button")
```

### Screenshot Capture

```python
# Capture full page screenshot
full_page_screenshot = get_page_screenshots(full_page=True)

# Capture specific element screenshot
element_screenshot = get_page_screenshots(selector="#main-content")
```

### Link Extraction

```python
# Get all links on the page
page_links = get_page_links()

# Get links matching a pattern
filtered_links = get_page_links(filter_pattern="contact")
```

### Multi-Tab Browsing

```python
# Create a new tab
tab_id = create_new_tab("https://example.com")

# Create another tab
another_tab_id = create_new_tab("https://example.org")

# List all open tabs
tabs = list_tabs()

# Switch between tabs
switch_tab(tab_id)

# Close a tab
close_tab(another_tab_id)
```

### Advanced Interactions

```python
# Scroll the page
scroll_page(direction="down", amount="page")

# Execute JavaScript on the page
result = execute_javascript("return document.title")

# Get detailed page information
page_info = get_page_info()

# Refresh the current page
refresh_page()

# Wait for navigation to complete
wait_for_navigation(timeout_ms=5000)
```

## 🛡️ Security Features

- SSL certificate validation bypass
- Secure browser context management
- Custom user-agent configuration
- Error handling and comprehensive logging
- Configurable timeout settings
- CSP bypass control
- Protection against cookie stealing

## 🔧 Troubleshooting

### Common Issues

- **SSL Certificate Errors**: Automatically bypassed
- **Slow Page Load**: Adjust timeout in `browse_to()` method
- **Element Not Found**: Verify selectors carefully
- **Browser Resource Usage**: Auto-cleanup after inactivity period

### Logging

All significant events are logged with detailed information for easy debugging.

## 📋 Tool Parameters

### `browse_to(url: str, context: Optional[Any] = None)`
- `url`: Website to navigate to
- `context`: Optional context object (currently unused)

### `extract_text_content(selector: Optional[str] = None, context: Optional[Any] = None)`
- `selector`: Optional CSS selector to extract specific content
- `context`: Optional context object (currently unused)

### `click_element(selector: str, context: Optional[Any] = None)`
- `selector`: CSS selector of the element to click
- `context`: Optional context object (currently unused)

### `get_page_screenshots(full_page: bool = False, selector: Optional[str] = None, context: Optional[Any] = None)`
- `full_page`: Capture entire page screenshot
- `selector`: Optional element to screenshot
- `context`: Optional context object (currently unused)

### `get_page_links(filter_pattern: Optional[str] = None, context: Optional[Any] = None)`
- `filter_pattern`: Optional text pattern to filter links
- `context`: Optional context object (currently unused)

### `input_text(selector: str, text: str, context: Optional[Any] = None)`
- `selector`: CSS selector of input element
- `text`: Text to input
- `context`: Optional context object (currently unused)

### `create_new_tab(url: Optional[str] = None, context: Optional[Any] = None)`
- `url`: Optional URL to navigate to in the new tab
- `context`: Optional context object (currently unused)

### `switch_tab(tab_id: str, context: Optional[Any] = None)`
- `tab_id`: ID of the tab to switch to
- `context`: Optional context object (currently unused)

### `list_tabs(context: Optional[Any] = None)`
- `context`: Optional context object (currently unused)

### `close_tab(tab_id: Optional[str] = None, context: Optional[Any] = None)`
- `tab_id`: Optional ID of the tab to close (defaults to current tab)
- `context`: Optional context object (currently unused)

### `refresh_page(context: Optional[Any] = None)`
- `context`: Optional context object (currently unused)

### `get_page_info(context: Optional[Any] = None)`
- `context`: Optional context object (currently unused)

### `scroll_page(direction: str = "down", amount: str = "page", context: Optional[Any] = None)`
- `direction`: Direction to scroll ('up', 'down', 'left', 'right')
- `amount`: Amount to scroll ('page', 'half', or a number)
- `context`: Optional context object (currently unused)

### `wait_for_navigation(timeout_ms: int = 10000, context: Optional[Any] = None)`
- `timeout_ms`: Maximum time to wait in milliseconds
- `context`: Optional context object (currently unused)

### `execute_javascript(script: str, context: Optional[Any] = None)`
- `script`: JavaScript code to execute
- `context`: Optional context object (currently unused)

## 🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

### Development Setup

```bash
# Clone the repository
git clone https://github.com/random-robbie/mcp-web-browser.git

# Create virtual environment
python -m venv venv
source venv/bin/activate  # On Windows use `venv\Scripts\activate`

# Install dependencies
pip install -e .[dev]
```

## 📄 License

MIT License

## 🔗 Related Projects

- [Model Context Protocol](https://modelcontextprotocol.io)
- [Playwright](https://playwright.dev)
- [Claude Desktop](https://claude.ai/desktop)

## 💬 Support

For issues and questions, please [open an issue](https://github.com/random-robbie/mcp-web-browser/issues) on GitHub.

```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "mcp-web-browser"
version = "0.1.0"
description = "MCP Web Browser Server using Playwright"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
    "mcp[cli]",
    "playwright",
]

[project.optional-dependencies]
dev = [
    "pytest",
    "ruff",
]

[tool.hatch.build.targets.wheel]
packages = ["src/mcp_web_browser"]

[tool.hatch.envs.default]
dependencies = [
    "pytest",
    "ruff",
]

[tool.ruff]
line-length = 100
target-version = "py310"

[tool.ruff.lint]
select = ["E", "F", "W", "I", "N", "UP", "ASYNC"]
ignore = ["E501"]

[project.scripts]
mcp-web-browser = "mcp_web_browser.server:main"

```

--------------------------------------------------------------------------------
/changelog.md:
--------------------------------------------------------------------------------

```markdown
Security Enhancements

Proper Error Handling and Logging: Added comprehensive logging with formatted output and proper error tracing
URL Sanitization: Automatically adds HTTPS prefix to URLs when needed
Content Security Policy Bypass Control: Added option to bypass CSP for better testing capabilities
Custom User-Agent: Configurable user-agent string
Security Headers: Added security-related HTTP headers to requests
JavaScript Execution Restrictions: Added validation to prevent cookie stealing via JavaScript

Performance Improvements

Browser Inactivity Timeout: Automatically closes browser after inactivity to free resources
Optimized Page Loading: Changed from 'networkidle' to 'domcontentloaded' for faster page loading
Resource Cleanup: Improved cleanup processes to ensure all resources are properly released
Retry Logic: Added retry mechanisms for page creation and element clicking
Efficient Screenshot Capture: Added JPEG compression for faster screenshot processing

Functional Additions

Multi-Tab Support: Added ability to create, switch between, and manage multiple tabs

create_new_tab: Opens a new browser tab
switch_tab: Switches between open tabs
list_tabs: Shows all open tabs and their information
close_tab: Closes a specific tab


Advanced Page Interaction:

refresh_page: Refreshes the current page
get_page_info: Provides detailed page information including metadata
scroll_page: Controls page scrolling in all directions
wait_for_navigation: Waits for page navigation to complete
execute_javascript: Safely executes JavaScript on the page


Enhanced Link Extraction:

Added filtering capabilities for links
Returns more metadata about links (title, text, etc.)


Improved Element Interaction:

Better visibility checking for elements
Automatic scrolling to ensure elements are in view
More reliable clicking with retry logic



Code Structure and Reliability

Better State Management: Added global state tracking for browser, pages, and tabs
Background Monitoring: Added inactivity monitor to clean up unused resources
Detailed Logging: Comprehensive logging throughout the system
Dynamic Playwright Import: Better error handling for Playwright import
Configurability: Added more configuration options for timeouts, viewport size, etc.

These improvements make the web browser MCP server more robust, secure, and feature-rich for your pentesting work, while ensuring resources are properly managed.

```

--------------------------------------------------------------------------------
/src/mcp_web_browser/server.py:
--------------------------------------------------------------------------------

```python
from typing import Optional, Union, Any
import asyncio
import base64
import sys
from mcp.server import FastMCP

# Create an MCP server for web browsing
mcp = FastMCP("Web Browser")

# Global browser and page management
_browser = None
_browser_context = None
_current_page = None
_playwright_instance = None

# Dynamic import of Playwright to avoid early import errors
def _import_playwright():
    from playwright.async_api import async_playwright
    return async_playwright

async def _ensure_browser():
    """Ensure a browser instance is available with SSL validation disabled"""
    global _browser, _browser_context, _playwright_instance
    
    if _browser is None:
        playwright_module = _import_playwright()
        _playwright_instance = await playwright_module().start()
        _browser = await _playwright_instance.chromium.launch()
        
        # Create a browser context that ignores HTTPS errors
        _browser_context = await _browser.new_context(
            ignore_https_errors=True,  # Ignore SSL certificate errors
        )
    return _browser, _browser_context

async def _close_current_page():
    """Close the current page if it exists"""
    global _current_page
    if _current_page:
        try:
            await _current_page.close()
        except Exception:
            pass
        _current_page = None

async def _safe_cleanup():
    """Safely clean up browser resources"""
    global _browser, _current_page, _browser_context, _playwright_instance
    
    try:
        if _current_page:
            try:
                await _current_page.close()
            except Exception:
                pass
        
        if _browser_context:
            try:
                await _browser_context.close()
            except Exception:
                pass
        
        if _browser:
            try:
                await _browser.close()
            except Exception:
                pass
        
        if _playwright_instance:
            try:
                await _playwright_instance.stop()
            except Exception:
                pass
    except Exception as e:
        # Log the error, but don't re-raise
        print(f"Error during cleanup: {e}", file=sys.stderr)
    finally:
        # Reset global variables
        _browser = None
        _browser_context = None
        _current_page = None
        _playwright_instance = None

@mcp.tool()
async def browse_to(url: str, context: Optional[Any] = None) -> str:
    """
    Navigate to a specific URL and return the page's HTML content.
    
    Args:
        url: The full URL to navigate to
        context: Optional context object for logging (ignored)
    
    Returns:
        The full HTML content of the page
    """
    global _current_page, _browser, _browser_context
    
    # Ensure browser is launched with SSL validation disabled
    _, browser_context = await _ensure_browser()
    
    # Close any existing page
    await _close_current_page()
    
    # Optional logging, but do nothing with context
    print(f"Navigating to {url}", file=sys.stderr)
    
    try:
        # Create a new page and navigate
        _current_page = await browser_context.new_page()
        
        # Additional options to handle various SSL/security issues
        await _current_page.goto(url, 
            wait_until='networkidle',
            timeout=30000,  # 30 seconds timeout
        )
        
        # Get full page content including dynamically loaded JavaScript
        page_content = await _current_page.content()
        
        # Optional: extract additional metadata
        try:
            title = await _current_page.title()
            print(f"Page title: {title}", file=sys.stderr)
        except Exception:
            pass
        
        return page_content
    
    except Exception as e:
        print(f"Error navigating to {url}: {e}", file=sys.stderr)
        raise

@mcp.tool()
async def extract_text_content(
    selector: Optional[str] = None, 
    context: Optional[Any] = None
) -> str:
    """
    Extract text content from the current page, optionally using a CSS selector.
    
    Args:
        selector: Optional CSS selector to target specific elements
        context: Optional context object for logging (ignored)
    
    Returns:
        Extracted text content
    """
    global _current_page
    
    if not _current_page:
        raise ValueError("No page is currently loaded. Use browse_to first.")
    
    try:
        if selector:
            # If selector is provided, extract text from matching elements
            elements = await _current_page.query_selector_all(selector)
            text_content = "\n".join([await el.inner_text() for el in elements])
            print(f"Extracted text from selector: {selector}", file=sys.stderr)
        else:
            # If no selector, extract all visible text from the page
            text_content = await _current_page.inner_text('body')
        
        return text_content
    
    except Exception as e:
        print(f"Error extracting text: {e}", file=sys.stderr)
        raise ValueError(f"Error extracting text: {e}")

@mcp.tool()
async def click_element(
    selector: str, 
    context: Optional[Any] = None
) -> str:
    """
    Click an element on the current page.
    
    Args:
        selector: CSS selector for the element to click
        context: Optional context object for logging (ignored)
    
    Returns:
        Confirmation message or error details
    """
    global _current_page
    
    if not _current_page:
        raise ValueError("No page is currently loaded. Use browse_to first.")
    
    try:
        element = await _current_page.query_selector(selector)
        if not element:
            raise ValueError(f"No element found with selector: {selector}")
        
        await element.click()
        print(f"Clicked element: {selector}", file=sys.stderr)
        
        return f"Successfully clicked element: {selector}"
    
    except Exception as e:
        print(f"Error clicking element: {e}", file=sys.stderr)
        raise ValueError(f"Error clicking element: {e}")

@mcp.tool()
async def get_page_screenshots(
    full_page: bool = False, 
    selector: Optional[str] = None,
    context: Optional[Any] = None
) -> str:
    """
    Capture screenshot of the current page.
    
    Args:
        full_page: Whether to capture the entire page or just the viewport
        selector: Optional CSS selector to screenshot a specific element
        context: Optional context object for logging (ignored)
    
    Returns:
        Base64 encoded screenshot image
    """
    global _current_page
    
    if not _current_page:
        raise ValueError("No page is currently loaded. Use browse_to first.")
    
    try:
        if selector:
            element = await _current_page.query_selector(selector)
            if not element:
                raise ValueError(f"No element found with selector: {selector}")
            screenshot_bytes = await element.screenshot()
        else:
            screenshot_bytes = await _current_page.screenshot(full_page=full_page)
        
        # Convert to base64 for easy transmission
        screenshot_base64 = base64.b64encode(screenshot_bytes).decode('utf-8')
        
        print(f"Screenshot captured: {'full page' if full_page else 'viewport'}", file=sys.stderr)
        
        return screenshot_base64
    
    except Exception as e:
        print(f"Error capturing screenshot: {e}", file=sys.stderr)
        raise ValueError(f"Error capturing screenshot: {e}")

@mcp.tool()
async def get_page_links(context: Optional[Any] = None) -> list[str]:
    """
    Extract all links from the current page.
    
    Args:
        context: Optional context object for logging (ignored)
    
    Returns:
        List of links found on the page
    """
    global _current_page
    
    if not _current_page:
        raise ValueError("No page is currently loaded. Use browse_to first.")
    
    try:
        # Use JavaScript to extract all links
        links = await _current_page.evaluate("""
            () => {
                const links = document.querySelectorAll('a');
                return Array.from(links).map(link => link.href);
            }
        """)
        
        print(f"Extracted {len(links)} links from the page", file=sys.stderr)
        
        return links
    
    except Exception as e:
        print(f"Error extracting links: {e}", file=sys.stderr)
        raise ValueError(f"Error extracting links: {e}")

@mcp.tool()
async def input_text(
    selector: str, 
    text: str, 
    context: Optional[Any] = None
) -> str:
    """
    Input text into a specific element on the page.
    
    Args:
        selector: CSS selector for the input element
        text: Text to input
        context: Optional context object for logging (ignored)
    
    Returns:
        Confirmation message
    """
    global _current_page
    
    if not _current_page:
        raise ValueError("No page is currently loaded. Use browse_to first.")
    
    try:
        element = await _current_page.query_selector(selector)
        if not element:
            raise ValueError(f"No element found with selector: {selector}")
        
        await element.fill(text)
        
        print(f"Input text into element: {selector}", file=sys.stderr)
        
        return f"Successfully input text into element: {selector}"
    
    except Exception as e:
        print(f"Error inputting text: {e}", file=sys.stderr)
        raise ValueError(f"Error inputting text: {e}")

def main():
    try:
        mcp.run()
    except Exception as e:
        print(f"Error running MCP server: {e}", file=sys.stderr)
    finally:
        # Use a separate event loop to ensure cleanup
        try:
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)
            loop.run_until_complete(_safe_cleanup())
            loop.close()
        except Exception as cleanup_error:
            print(f"Cleanup error: {cleanup_error}", file=sys.stderr)

if __name__ == "__main__":
    main()

```