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_cffito 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
Web UI (Recommended)
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"]
- Fetch — Downloads the page HTML using
curl_cffiwith Chrome browser impersonation to bypass anti-bot measures. Retries up to 3 times with exponential backoff. - Pre-process — Unwraps nested tags inside
<pre>blocks with BeautifulSoup to prevent code block fragmentation in Trafilatura's output. - 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.).
- Clean — Post-processes the Markdown: downgrades header levels, merges broken definition lists, and removes empty bullets.
- 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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| mdscrape-1.0.0.tar.gz | 15.2 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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.