browsing 0.1.5

Browser automation: navigate, click, extract, screenshot. Standalone browser control via CDP.
Documentation

Browsing

Lightweight, headless-first MCP/API for browser automation

A concise MCP server and Rust library: navigate, get_links, follow_link, list_content (links+images), get_content, get_image, save_content, screenshot (full or element). Lazy browser init. Parallel reads via RwLock. Runs headless by default with 17+ lightweight Chrome flags for fast, minimal operation. No LLM or AI dependencies.

🎯 Usage Modes

  1. πŸ”Œ MCP Server (primary) - navigate, get_links, follow_link, list_content, get_content, get_image, save_content, screenshot, generate_sitemap tools
  2. ⌨️ CLI - Browser automation tasks
  3. πŸ“¦ Library - Browser automation via CDP

✨ Why Browsing?

Browser automation is challenging. You need to:

  • Extract structured data from unstructured HTML - Parse complex DOM trees into usable representations
  • Handle browser automation reliably - Manage browser lifecycle, CDP connections, and process management
  • Maintain testability - Mock components for unit testing without real browsers
  • Support extensibility - Add custom actions and browser backends

Browsing solves all of this with a clean, modular, and well-tested architecture.

🧠 Why Better Than Chrome DevTools?

Chrome DevTools Protocol (CDP) is an engine β€” it gives you raw browser control, but you must build all intelligence yourself. Most "headless browsing" tools are just CDP wrappers with slightly nicer APIs.

Browsing is a self-driving car on top of that engine. It adds five intelligence layers that are hard to replicate by simply wrapping CDP:

Layer CDP (Raw Protocol) Browsing (Browser Automation)
Perception Raw DOM tree Semantic understanding: page intent, form schemas, action affordances
Action Brittle backend_node_id/XPath Self-healing element resolution: index β†’ text β†’ semantic fallback
Memory Nothing persists Session history, site-specific strategies
Observability Protocol logs DOM state snapshots and action traces

The goal: CDP becomes invisible. Users think about tasks, not WebSocket messages or element IDs.

🎯 Key Features

πŸ—οΈ Trait-Based Architecture

  • BrowserClient trait - Abstract browser operations for easy mocking and alternative backends
  • DOMProcessor trait - Pluggable DOM processing implementations
  • ActionHandler trait - Extensible action system for custom behaviors

🌐 Full Browser Automation

  • Headless by default β€” BrowserProfile::default() runs headless with --headless=new
  • 17+ lightweight flags β€” disables extensions, sync, background networking, logging, and more
  • Cross-platform support (macOS, Linux, Windows)
  • Automatic browser detection
  • Chrome DevTools Protocol (CDP) integration
  • Tab management (create, switch, close)
  • Screenshot capture (page and element-level)

πŸ“Š Advanced DOM Processing

  • Full CDP integration (DOM, AX tree, Snapshot)
  • Text serialization with interactive element indices
  • Accessibility tree support for better semantic understanding

πŸ”§ Extensible & Maintainable

  • Manager-based architecture (TabManager, NavigationManager, ScreenshotManager)
  • Custom action registration
  • Utility traits for reduced code duplication
  • Comprehensive test coverage (350+ tests)

πŸ“¦ Installation

As a Library

[dependencies]
browsing = "0.1"
tokio = { version = "1.40", features = ["full"] }

As a CLI Tool

cargo install --path . --bin browsing

As an MCP Server

cargo build --release --bin browsing-mcp

πŸš€ οΏ½ Quick Start## οΏ½ Quick Start

1️⃣ CLI Usage

# Run a browser automation task (headless by default)
browsing run "Navigate to example.com and extract content" --url https://news.ycombinator.com

# Run with visible browser
browsing run "Navigate to example.com and extract content" --url https://news.ycombinator.com --no-headless

# Launch a headless browser and get CDP URL (headless by default)
browsing launch

# Launch with visible browser
browsing launch --no-headless

# Connect to existing browser
browsing connect ws://localhost:9222/devtools/browser/abc123

πŸ“– Full CLI Documentation

2️⃣ MCP Server Usage

Configure in Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json):

{
  "mcpServers": {
    "browsing": {
      "command": "/path/to/browsing/target/release/browsing-mcp",
      "env": {
        "BROWSER_USE_HEADLESS": "true"
      }
    }
  }
}

Then ask Claude:

"Navigate to rust-lang.org, get the links, follow the second link, and screenshot the main content area"

πŸ“– Full MCP Documentation

3️⃣ Library Usage

use anyhow::Result;
use browsing::Browser;
use browsing::browser::BrowserProfile;

#[tokio::main]
async fn main() -> Result<()> {
    browsing::init();

    let mut browser = Browser::new(BrowserProfile::default());
    browser.start().await?;

    browser.navigate("https://example.com").await?;

    let title = browser.get_current_page_title().await?;
    println!("Title: {}", title);

    let _ = browser.stop().await;
    Ok(())
}

πŸ“– Full Library Documentation

Browser Launch Options

use browsing::{Browser, BrowserProfile};

// Option 1: Auto-launch headless browser (default)
let profile = BrowserProfile::default(); // headless = true by default
let browser = Browser::new(profile);

// Option 2: Launch with visible browser
let profile = BrowserProfile::default().with_headless(false);
let browser = Browser::new(profile);

// Option 3: Connect to existing browser
let browser = Browser::new(BrowserProfile::default())
    .with_cdp_url("http://localhost:9222".to_string());

// Option 4: Custom browser executable
use browsing::browser::launcher::BrowserLauncher;
let launcher = BrowserLauncher::new(profile)
    .with_executable_path(std::path::PathBuf::from("/path/to/chrome"));

Using Traits for Testing

Standalone Browser Testing (no AI required)

use browsing::traits::{BrowserClient, DOMProcessor};
use std::sync::Arc;

// Test your browser automation logic directly

πŸ“š Usage Examples

Content Download

use browsing::{Browser, BrowserProfile};
use browsing::dom::DOMProcessorImpl;
use browsing::traits::DOMProcessor;

#[tokio::main]
async fn main() -> browsing::error::Result<()> {
    let mut browser = Browser::new(BrowserProfile::default());
    browser.start().await?;

    // Navigate to website
    browser.navigate("https://www.ibm.com").await?;
    tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;

    // Extract content
    let cdp_client = browser.get_cdp_client()?;
    let session_id = browser.get_session_id()?;
    let target_id = browser.get_current_target_id()?;

    let dom_processor = DOMProcessorImpl::new()
        .with_cdp_client(cdp_client, session_id)
        .with_target_id(target_id);

    let state = dom_processor.get_serialized_dom().await?;
    let page_content = state.text_representation(None).unwrap_or_default();
    println!("Extracted {} bytes of content", page_content.len());

    // Save to file
    std::fs::write("ibm_content.txt", page_content)?;
    Ok(())
}

Run this example:

cargo run --example ibm_content_download

Screenshot Capture

use browsing::Browser;

let browser = Browser::new(BrowserProfile::default());
browser.start().await?;

// Full page screenshot
let screenshot_data = browser.take_screenshot(
    Some("screenshot.png"),  // path
    true,                     // full_page
    None,                     // format
    None,                     // quality
).await?;

// Viewport only
let viewport = browser.take_screenshot(
    Some("viewport.png"),
    false,
    None,
    None,
).await?;

Direct Browser Control

use browsing::{Browser, BrowserProfile};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut browser = Browser::new(BrowserProfile::default());
    browser.start().await?;

    // Navigate
    browser.navigate("https://example.com").await?;

    // Get current URL
    let url = browser.get_current_url().await?;
    println!("Current URL: {}", url);

    // Tab management
    use browsing::traits::BrowserClient;
    browser.create_tab(Some("https://hackernews.com")).await?;
    let tabs = browser.get_tabs().await?;
    println!("Open tabs: {}", tabs.len());

    // Switch tabs
    browser.switch_to_tab(&tabs[0].target_id).await?;

    Ok(())
}

Custom Actions

use browsing::tools::views::{ActionHandler, ActionParams, ActionContext, ActionResult};
use browsing::error::Result;
use browsing::tools::Tools;

struct CustomActionHandler;

#[async_trait::async_trait]
impl ActionHandler for CustomActionHandler {
    async fn execute(
        &self,
        params: &ActionParams,
        context: &mut ActionContext<'_>,
    ) -> Result<ActionResult> {
        // Custom action logic here
        Ok(ActionResult {
            extracted_content: Some("Custom result".to_string()),
            ..Default::default()
        })
    }
}

// Register custom action
let mut tools = Tools::default();
tools.register_custom_action(
    "custom_action".to_string(),
    "Description of custom action".to_string(),
    None,  // domains
    CustomActionHandler,
);

πŸ—οΈ Architecture

Browsing follows SOLID principles with a focus on separation of concerns, testability, and maintainability.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Browser Automation                       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚   Browser   β”‚ DOMProcessor β”‚        Tools            β”‚  β”‚
β”‚  β”‚   (trait)   β”‚    (trait)   β”‚                         β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚         β”‚             β”‚                      β”‚           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚             β”‚                      β”‚
    β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β–Όβ”€β”€β”€β”€β”        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”
    β”‚  Browser   β”‚ β”‚DomSvc  β”‚        β”‚   Handlers   β”‚
    β”‚            β”‚ β”‚        β”‚        β”‚              β”‚
    β”‚TabManager  β”‚ β”‚CDP     β”‚        β”‚Navigation    β”‚
    β”‚NavManager  β”‚ β”‚HTML    β”‚        β”‚Interaction   β”‚
    β”‚Screenshot  β”‚ β”‚Tree    β”‚        β”‚Tabs          β”‚
    β”‚            β”‚ β”‚Builder β”‚        β”‚Content       β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key Components

Component Responsibility Trait-Based
Browser Manages browser session and lifecycle Implements BrowserClient
DOMProcessor Extracts and serializes DOM Implements DOMProcessor
Tools Action registry and execution Uses BrowserClient trait
Handlers Specific action implementations Use ActionHandler trait

πŸ“ Project Structure

browsing/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ agent/              # Agent view types (used by tools layer)
β”‚   β”‚   β”œβ”€β”€ views.rs        # Data types
β”‚   β”‚   └── memory.rs       # Session memory types
β”‚   β”œβ”€β”€ browser/            # Browser management
β”‚   β”‚   β”œβ”€β”€ session.rs      # Browser session (BrowserClient impl)
β”‚   β”‚   β”œβ”€β”€ tab_manager.rs  # Tab operations
β”‚   β”‚   β”œβ”€β”€ navigation.rs   # Navigation operations
β”‚   β”‚   β”œβ”€β”€ screenshot.rs   # Screenshot operations
β”‚   β”‚   β”œβ”€β”€ cdp.rs          # CDP WebSocket client
β”‚   β”‚   β”œβ”€β”€ launcher.rs     # Browser launcher
β”‚   β”‚   └── profile.rs      # Browser configuration
β”‚   β”œβ”€β”€ dom/                # DOM processing
β”‚   β”‚   β”œβ”€β”€ processor.rs    # DOMProcessor trait impl
β”‚   β”‚   β”œβ”€β”€ serializer.rs   # Text serialization
β”‚   β”‚   β”œβ”€β”€ tree_builder.rs # DOM tree construction
β”‚   β”‚   β”œβ”€β”€ cdp_client.rs   # CDP wrapper for DOM
β”‚   β”‚   └── html_converter.rs # HTML to markdown
β”‚   β”œβ”€β”€ tools/              # Action system
β”‚   β”‚   β”œβ”€β”€ service.rs      # Tools registry
β”‚   β”‚   β”œβ”€β”€ handlers/       # Action handlers
β”‚   β”‚   β”‚   β”œβ”€β”€ navigation.rs
β”‚   β”‚   β”‚   β”œβ”€β”€ interaction.rs
β”‚   β”‚   β”‚   β”œβ”€β”€ tabs.rs
β”‚   β”‚   β”‚   β”œβ”€β”€ content.rs
β”‚   β”‚   β”‚   └── advanced.rs
β”‚   β”‚   └── params.rs       # Parameter extraction
β”‚   β”œβ”€β”€ traits/             # Core trait abstractions
β”‚   β”‚   β”œβ”€β”€ browser_client.rs  # BrowserClient trait
β”‚   β”‚   └── dom_processor.rs   # DOMProcessor trait
β”‚   β”œβ”€β”€ actor/              # Low-level interactions
β”‚   β”‚   β”œβ”€β”€ page.rs         # Page operations
β”‚   β”‚   β”œβ”€β”€ element.rs      # Element operations
β”‚   β”‚   β”œβ”€β”€ mouse.rs        # Mouse interactions
β”‚   β”‚   └── keyboard.rs     # Keyboard input
β”‚   β”œβ”€β”€ config.rs           # Configuration
β”‚   β”œβ”€β”€ error.rs            # Error types
β”‚   β”œβ”€β”€ logging.rs          # Logging setup
β”‚   β”œβ”€β”€ metrics.rs          # Metrics collection
β”‚   β”œβ”€β”€ utils.rs            # Utilities
β”‚   └── views.rs            # Shared data types
└── Cargo.toml

🎨 Design Principles

Trait-Facing Design

  • BrowserClient - Abstract browser operations for testing and alternative backends
  • DOMProcessor - Pluggable DOM processing implementations
  • ActionHandler - Extensible action system

Separation of Concerns

  • TabManager - Tab operations (create, switch, close)
  • NavigationManager - Navigation logic
  • ScreenshotManager - Screenshot capture
  • Handlers - Focused action implementations

DRY (Don't Repeat Yourself)

  • ActionParams - Reusable parameter extraction
  • JSONExtractor - Centralized JSON parsing
  • SessionGuard - Unified session access

KISS (Keep It Simple, Stupid)

  • Split complex methods into focused helpers
  • Clear naming and single responsibility
  • Minimal dependencies between modules

πŸ§ͺ Testing

# Run all tests
cargo test

# Run with output
cargo test -- --nocapture

# Run specific test
cargo test test_browser_navigation

# Run integration tests only
cargo test --test integration

Test Coverage

⚠️ Data Retention Policy

Browser Data is NEVER Deleted

IMPORTANT: The browsing library never deletes browser data for safety reasons.

What This Means:

Data Type Behavior
Bookmarks Never deleted
History Never deleted
Cookies Never deleted
Passwords Never deleted
Extensions Never deleted
Cache Never deleted
Temp Directories Never deleted (left in /tmp/)

Why This Policy Exists:

  1. User Safety: Users may specify a custom user_data_dir pointing to their real browser profile
  2. Catastrophe Prevention: Accidentally deleting a user's real browser data (bookmarks, history, passwords) would be devastating
  3. Debugging: Leaving temp directories allows inspection after crashes or failures
  4. User Control: Users are responsible for managing their own browser data

How It Works:

When no user_data_dir is specified:

let profile = BrowserProfile {
    user_data_dir: None,  // Uses temp directory: /tmp/browser-use-1738369200000/
    ..Default::default()
};

When browser.stop() is called:

  • βœ… Browser process is killed
  • βœ… In-memory state is cleared
  • ❌ User data directory is NOT deleted

Managing Temporary Data:

Users are responsible for cleanup:

# List browser temp directories
ls -la /tmp/browser-use-*

# Delete old temp directories (optional, manual cleanup)
rm -rf /tmp/browser-use-1738369200000/

Using a Custom Data Directory:

let profile = BrowserProfile {
    user_data_dir: Some("/path/to/custom/profile".into()),
    ..Default::default()
};

Warning: If you point to your real browser profile, the library will NOT protect it. You're responsible for that directory.

πŸ”§ Configuration

Browser Profile

use browsing::BrowserProfile;

// Default: headless mode with lightweight flags
let profile = BrowserProfile::default();

// Explicit headless
let profile = BrowserProfile::default().with_headless(true);

// Visible browser for debugging
let profile = BrowserProfile::default().with_headless(false);

// With custom data directory
let profile = BrowserProfile {
    user_data_dir: Some("/path/to/profile".into()),
    ..Default::default()
};

πŸ“– API Documentation

Generate and view API docs:

cargo doc --open