Skip to main content

AI Web Scraper

A robust web scraper that extracts webpage content as clean Markdown, served via a FastAPI backend with a real-time streaming web UI.

Features

  • MCP Server — Exposes scraping tools via the Model Context Protocol so AI agents (Claude Desktop, Cursor, Antigravity, etc.) can scrape web pages directly
  • Real-time Web UI — Custom HTML/CSS/JS dashboard powered by FastAPI and Server-Sent Events (SSE) for live progress updates
  • Smart content extraction — Trafilatura-first with BeautifulSoup pre-processing for maximum coverage
  • Anti-bot bypass — Uses curl_cffi to impersonate real browsers (Chrome TLS fingerprint)
  • Retry with backoff — Automatic retries with exponential backoff on failed requests
  • Markdown output — Clean Markdown with title, source URL, and extracted content
  • Rich metadata — Returns author, date, description, site name, language, word count, and Open Graph image alongside content
  • Batch scraping — Scrape multiple URLs from a text file or the web UI
  • Sitemap support — Extract URLs from XML sitemaps (including sitemap indexes) with recursive resolution
  • URL validation — Validates all URLs before attempting to fetch
  • Markdown cleanup — Fixes header levels, merges broken definition lists, and removes empty bullets from Trafilatura output

Installation

Requires Python 3.12+ and uv.

# Clone the repository
git clone https://github.com/SouravKAgarwal/web-scraper.git
cd web-scraper

# Install dependencies
uv sync

Usage

Launch the FastAPI server to use the web interface:

uvicorn server:app --host 0.0.0.0 --port 8000 --reload

This starts the server at http://localhost:8000. Open it in your browser to:

  • Enter one or more URLs (one per line) or a sitemap URL
  • Set an optional limit on the number of pages to scrape
  • Watch real-time progress as each page is scraped
  • View rendered Markdown results with syntax highlighting

MCP Server (for AI Agents)

Run MDScrape as an MCP server so AI coding assistants can scrape web pages directly:

uv run mcp_server.py

Configure in Claude Desktop / Cursor / Antigravity:

{
  "mcpServers": {
    "mdscrape": {
      "command": "uv",
      "args": ["run", "--directory", "/path/to/web-scraper", "mcp_server.py"]
    }
  }
}

Available tools:

Tool Description
scrape_url Scrape a single page → Markdown + metadata
scrape_urls Scrape multiple pages in batch
scrape_sitemap Crawl a sitemap and scrape all pages
extract_sitemap_urls List all URLs in a sitemap without scraping

CLI Usage

mdscrape is globally installed on your system. You can run it directly from any directory:

# Scrape a single URL
mdscrape https://example.com/article

# Scrape multiple URLs from a file
mdscrape -f urls.txt

# Scrape with limit
mdscrape -f urls.txt -n 10

# Directly crawl and scrape pages from a sitemap
mdscrape -s https://example.com/sitemap.xml -n 20

# Extract URLs from a sitemap without scraping (print to stdout)
mdscrape -s https://example.com/sitemap.xml --urls-only

Project Structure

web-scraper/
├── cli.py               # Main CLI interface (URLs, files, sitemaps)
├── mcp_server.py        # MCP server exposing scraping tools for AI agents
├── server.py            # FastAPI server serving Web UI and /scrape API
├── helpers/             # Utility functions
│   ├── http.py          # HTTP fetch with retries, URL validation
│   └── markdown.py      # Markdown post-processing and cleanup
├── services/            # Core business logic
│   ├── scraper.py       # Page fetching, BS4 + Trafilatura extraction
│   └── sitemap.py       # XML sitemap parsing & recursive traversal
├── static/              # Web UI assets
│   ├── index.html       # UI layout
│   ├── script.js        # SSE stream listener & renderer
│   └── style.css        # UI styling
├── pyproject.toml       # Project metadata & dependencies
├── uv.lock              # Dependency lockfile
└── .python-version      # Python version (3.12)

How It Works

flowchart LR
    A["URL Input"] --> B["curl_cffi Fetch"]
    B --> C["BS4 Pre-process"]
    C --> D{"Trafilatura\nExtract"}
    D -- "Success" --> E["Clean Markdown"]
    D -- "Fail" --> F["Error"]
    E --> G["Return JSON\nwith Metadata"]
  1. Fetch — Downloads the page HTML using curl_cffi with Chrome browser impersonation to bypass anti-bot measures. Retries up to 3 times with exponential backoff.
  2. Pre-process — Unwraps nested tags inside <pre> blocks with BeautifulSoup to prevent code block fragmentation in Trafilatura's output.
  3. Extract — Passes the cleaned HTML to Trafilatura for intelligent content extraction as Markdown (with links and images). Also extracts metadata (title, author, date, description, etc.).
  4. Clean — Post-processes the Markdown: downgrades header levels, merges broken definition lists, and removes empty bullets.
  5. Return — Returns structured JSON containing the Markdown content and all extracted metadata.

API Reference

POST /scrape

Streams scrape results via Server-Sent Events (SSE).

Request body:

{
  "urls": ["https://example.com/page1", "https://example.com/page2"],
  "limit": 0
}

Request Example:

import json

import requests

result = {}


def scrape_result(urls: list, limit: int = 0) -> dict:
    resp = requests.post(
        url="http://localhost:8000/scrape",
        json={"urls": urls, "limit": limit},
        stream=True,
    )

    for line in resp.iter_lines(decode_unicode=True):
        if line.startswith("data:"):
            event = json.loads(line[len("data:") :])
            if event["type"] == "done":
                del event["type"]
                result.update(event)

    return result


if __name__ == "__main__":
    urls =  ["https://example.com/page1", "https://example.com/page2"]
    limit = 2

    result = scrape_result(urls, limit)
    print(json.dumps(result, indent=4))
Field Type Description
urls list[str] URLs to scrape, or a single sitemap URL if sitemap is true
sitemap bool Treat the first URL as a sitemap and extract URLs from it
limit int Max URLs to scrape (0 = unlimited)

SSE event types:

type Description
progress Sent before scraping each URL (current, total, url)
result Scraped data for one URL (or error)
info Informational messages (e.g., sitemap extraction count)
done Final summary (successful, failed, total, response_time)
error Fatal error (e.g., no valid URLs)

CLI Reference

main.py

Flag Description Default
url A single URL to scrape (positional) —
-f, --file Text file with URLs (one per line) —
-s, --sitemap Sitemap XML URL to crawl —
-n, --limit Max number of URLs to process Unlimited
--urls-only Print sitemap URLs without scraping False

If no URL, file, or sitemap is provided, prompts for manual input.

Output is printed as JSON to stdout (or URLs list with --urls-only).


License

MIT

Metadata

Release files for mdscrape 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mdscrape 1.0.0
File Size Uploaded Provenance
mdscrape-1.0.0.tar.gz 15.2 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for mdscrape 1.0.0
File Interpreter ABI Platform
mdscrape-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 29.1 kB

Release files / mdscrape-1.0.0.tar.gz

Download URL mdscrape-1.0.0.tar.gz
Size 15.2 kB
Tags Source
SHA-256 checksum
How to use checksums
eb8396960b17a17595dd444eca4265d9f97c9324a788da47e6ad5e57611d6822
BLAKE2b-256 checksum
How to use checksums
e975d95201a6b3bcd314cc305a9a1e7a264585464571959a2a2218a36e356a67
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / mdscrape-1.0.0-py3-none-any.whl

Download URL mdscrape-1.0.0-py3-none-any.whl
Size 13.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8e4a0746ad6cbf14214fab8c3b09263a3450895f96d956b37db88cd437c1bb45
BLAKE2b-256 checksum
How to use checksums
b5927eb49c637ab6478628a428ebcff99ec72120284c8bf3cb9fd89845abd891
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for mdscrape 1.0.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page