Proxy Browser MCP Server
by nullsection
README.md
# Proxy Browser — MCP Server
An [MCP](https://modelcontextprotocol.io) server that lets Claude browse the web and make
**GET / POST / PUT / PATCH / DELETE / HEAD** requests while looking like a real **Chrome**
browser at the network layer — so you don't get anti-bot flagged on sites like AliExpress.
- Real Chrome TLS/HTTP2 fingerprint (via `curl_cffi`), not just a spoofed User-Agent
- **Headless-browser render tier** — falls back to a real, undetected Chromium (patchright)
that executes JavaScript and passes JS anti-bot challenges (Cloudflare, DataDome, AliExpress)
- **Anti-bot challenge detection** — flags captcha/punish walls instead of returning the stub as content
- Persistent cookie session — multi-step browsing looks like one continuous browser
- Token-efficient output — HTML is cleaned into markdown by default (raw available)
- Streamed, byte-capped body reads — an oversized/binary response can't blow up memory
- Automatic retries with backoff + jitter (honors `Retry-After`) for idempotent methods
- Randomized think-time + a hard 1–2 request cap per call — a constant, bursty cadence is itself a bot tell
- Optional upstream proxy — plug in a residential/rotating proxy for IP rotation
- Full verb set with form or JSON bodies
---
## Why not just `requests` + a Chrome User-Agent?
Because a UA string fools nobody serious. Cloudflare, Akamai, and AliExpress fingerprint
your **TLS handshake (JA3/JA4)** and **HTTP/2 frame ordering** — a stock Python HTTP client
is instantly identifiable as a bot no matter what `User-Agent` header it sends.
This server uses [`curl_cffi`](https://github.com/lexiforest/curl_cffi), which drives
libcurl with a **real Chrome TLS + HTTP/2 fingerprint**. The connection itself looks like
Chrome — verified against `tls.peet.ws` (JA4 `t13d1516h2`, Chrome's Akamai HTTP/2
fingerprint, over `h2`).
---
## Requirements
- **Python 3.9+** (developed/tested on 3.11, Windows 11)
- **pip**
- **Claude Code** CLI (to consume the MCP server)
- Python packages (installed via `requirements.txt`):
| Package | Role |
| ---------------- | ------------------------------------------------------- |
| `mcp` | Official MCP Python SDK (FastMCP server) |
| `curl_cffi` | Chrome TLS/HTTP2 impersonation engine (bundled libcurl) |
| `trafilatura` | Main-content extraction → markdown (token-efficient) |
| `beautifulsoup4` | Full-page DOM cleanup |
| `markdownify` | HTML → markdown conversion |
`curl_cffi` ships its own libcurl build, so there is **no external C dependency** to install.
---
## Installation
```powershell
# 1. Clone the repo
git clone https://github.com/nullsection/headless-proxy-mcp.git
cd headless-proxy-mcp
# 2. (recommended) create a virtual environment
python -m venv .venv
.\.venv\Scripts\Activate.ps1 # PowerShell
# source .venv/bin/activate # macOS/Linux
# 3. Install dependencies
python -m pip install -r requirements.txt
```
---
## Setup — register with Claude Code
```powershell
# User scope = available in all your projects
claude mcp add -s user proxy-browser -- python C:/path/to/headless-proxy-mcp/proxy_browser_server.py
# Verify
claude mcp get proxy-browser # should show: Status: Connected
```
> Use forward slashes in the path — a backslash path can be mangled by the shell.
To pass configuration (e.g. a proxy), add `-e` flags:
```powershell
claude mcp add -s user proxy-browser \
-e PROXY_URL=http://user:pass@host:port \
-e IMPERSONATE=chrome124 \
-- python C:/path/to/headless-proxy-mcp/proxy_browser_server.py
```
**Restart Claude Code after registering** so it loads the server.
---
## Usage
Once registered and restarted, just ask Claude in natural language — e.g.
*"browse aliexpress for usb cables and summarize the top results."* Claude selects the
right tool automatically.
### Tools
| Tool | Description |
| --------------- | ------------------------------------------------------------------- |
| `browse` | HTTP **GET** a URL, with an optional headless-browser fallback (`render` policy). |
| `render` | Load a URL in a **real headless browser** (executes JS; passes JS challenges). |
| `http_post` | HTTP **POST** with form `data` or `json` body. |
| `http_put` | HTTP **PUT** with form `data` or `json` body. |
| `http_patch` | HTTP **PATCH** with form `data` or `json` body. |
| `http_delete` | HTTP **DELETE** (optional `data` or `json` body). |
| `http_head` | HTTP **HEAD** — response headers only (liveness / type / redirect). |
| `session_reset` | Clear cookies / start a fresh browsing identity. |
| `session_info` | Show impersonation target, proxy, config, and current cookies. |
> **Retries & idempotency:** `browse`, `http_put`, `http_delete`, and `http_head` are
> idempotent, so transient `429`/`5xx`/network failures are retried automatically with
> exponential backoff (honoring `Retry-After`). `http_post` and `http_patch` are **not**
> retried, to avoid duplicate side effects.
### Parameters
- **`url`** — full URL including `http://` or `https://`.
- **`extract`** — how the body is returned:
- `"auto"` (default) — main content as markdown (best for pages)
- `"full"` — the whole page converted to markdown
- `"text"` — plain text only
- `"raw"` — exact HTML / response bytes
- **`headers`** — optional extra request headers (merged over sane Chrome defaults).
- **`proxy`** — optional per-request proxy URL (overrides `PROXY_URL` for that call).
- **`timeout`** — request timeout in seconds (default 30).
- **`data`** / **`json`** (POST/PUT only) — form fields **or** a JSON body (use one).
---
## Configuration
Set via environment variables (or `-e` flags at registration). See `.env.example`.
| Variable | Default | Description |
| -------------------- | ----------- | ------------------------------------------------------------------ |
| `IMPERSONATE` | `chrome131` | Chrome build to mimic (e.g. `chrome124`, `chrome131`, `chrome136`). Keep it recent. |
| `PROXY_URL` | *(empty)* | Upstream proxy: `http://user:pass@host:port` or `socks5://host:port`. Empty = direct. |
| `MAX_CHARS` | `60000` | Max characters of the **extracted** body returned to Claude (protects context). |
| `MAX_RESPONSE_BYTES` | `10000000` | Hard cap on **raw** bytes read off the wire; body is streamed and reading stops past this. |
| `PER_HOST_DELAY` | `0` | Minimum seconds between requests to the same host (politeness / anti-flag). |
| `PER_HOST_JITTER` | `0.3` | Randomize the per-host delay by ±this fraction (0.3 = ±30%) so cadence isn't constant. |
| `MAX_RETRIES` | `2` | Additional attempts after the first on `429`/`5xx`/network errors (bounded by `MAX_REQUESTS_PER_CALL`). |
| `RETRY_BACKOFF` | `0.5` | Base retry backoff in seconds, grown exponentially with jitter. |
| `MAX_RETRY_WAIT` | `30` | Cap on any single retry sleep, even if `Retry-After` asks for more. |
| `REQUEST_JITTER_MIN` / `REQUEST_JITTER_MAX` | `0.5` / `2.0` | Randomized think-time (s) before **every** request — fires even on a single hit, so nothing goes out with bot-like zero latency. `MAX=0` disables. |
| `MAX_REQUESTS_PER_CALL` | `2` | Hard ceiling on total HTTP hits per tool call (initial + retries), clamped to **1–2** to stay under anti-bot thresholds. |
| `RENDER_MODE` | `auto` | Browser-fallback policy for `browse`: `auto` \| `never` \| `always`. |
| `RENDER_WAIT_UNTIL` | `domcontentloaded` | Load state renders wait for; use `networkidle` for XHR-driven grids. |
| `RENDER_HEADLESS` | `1` | `0` = headful window (max stealth, needs a display). |
| `RENDER_CHANNEL` | *(bundled)* | `chrome`/`msedge` to drive the real installed browser (new headless, real GPU). Recommended for hard walls. |
| `RENDER_USER_DATA_DIR` | *(none)* | Persistent Chrome profile dir — banks anti-bot cookies + logins across runs (the PerimeterX lever). |
| `RENDER_EXTRA_ARGS` | *(none)* | Extra Chromium flags, comma-separated (e.g. `--use-angle=d3d11` for real-GPU WebGL on Windows). |
| `RENDER_LOCALE` / `RENDER_TIMEZONE` / `RENDER_VIEWPORT` / `RENDER_UA` | `en-US` / `America/New_York` / `1280x800` / *(auto)* | Realistic browser context. UA defaults to the engine's real UA with the `HeadlessChrome` tell stripped. |
See `.env.example` for the full render knob list (`RENDER_TIMEOUT`, `RENDER_WAIT_MS`, `RENDER_MIN_CONTENT_CHARS`).
---
## Troubleshooting
- **`Failed to connect`** — the path was likely mangled by backslashes. Re-add using
**forward slashes** (see setup above), and confirm `python` is on your PATH.
- **Server connects but a site still blocks you** — you're probably being flagged by **IP**,
not fingerprint. Set `PROXY_URL` to a residential/rotating proxy and raise `PER_HOST_DELAY`.
- **Page comes back blank / missing content** — the site renders via JavaScript. `curl_cffi`
does not execute JS (see Limitations).
- **Response truncated** — raise `MAX_CHARS`, or request a tighter `extract` mode.
---
## Headless browser render tier
The HTTP engine mimics Chrome's *network identity* but can't run JavaScript, so JS-rendered
pages and JS challenge walls defeat it. The optional render tier fixes that by driving a
**real, undetected Chromium** ([patchright](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright),
a patched Playwright drop-in) that executes JS, passes challenges, and hides the automation
tells (`navigator.webdriver`, the `HeadlessChrome` UA).
```powershell
pip install -r requirements-render.txt
patchright install chromium # one-time browser download (or: playwright install chromium)
```
- **`browse(url, render="auto")`** (default) — tries fast HTTP first, then transparently
escalates to the browser when the page looks challenge-walled or JS-thin **and** an engine
is installed. `render="never"` = HTTP only; `render="always"` = browser only.
- **`render(url, wait_selector=".product-card", wait_ms=4000)`** — explicit browser load.
Use `wait_selector`/`wait_ms` (or `RENDER_WAIT_UNTIL=networkidle`) for grids that populate
via XHR after load.
> Verified live: this tier passes AliExpress's `_tmd_/punish` captcha wall that blocks the HTTP
> client, and renders the JS product grid. `navigator.webdriver` is hidden and the UA is cleaned
> to a real Chrome string.
### Getting past hard walls (PerimeterX / DataDome / Facebook)
For aggressive bot managers, the render tier has a **persistent-profile + organic-navigation**
mode. Verified live getting past **Carsales' PerimeterX** wall (which `403`s a cold session):
- `RENDER_CHANNEL=chrome` + `RENDER_USER_DATA_DIR=<dedicated dir>` + `RENDER_EXTRA_ARGS=--use-angle=d3d11`
- `render(url, warm=[...homepage, category...])` — step down to the deep page so the anti-bot
cookie matures instead of cold-linking.
- `render(url, press=["Escape"])` — dismiss **Facebook Marketplace's** login overlay.
**See [docs/anti-bot-bypass.md](docs/anti-bot-bypass.md) for the full playbook** — the wall tiers,
the detection-signal table, the exact PerimeterX recipe, pacing/cooldown notes, and when to reach
for a managed unblocker instead.
## Testing
Unit tests are **hermetic** — they stub `curl_cffi`/`mcp` and mock the browser, so no native
libs, browser, or network are needed. They cover the request/retry loop, the streamed byte cap,
`Retry-After` handling, challenge detection, extraction dispatch, the tiered render fallback,
and the render engine (including its asyncio-safety) — **~92% line coverage across both modules**.
```powershell
python -m pip install -r requirements-dev.txt
python -m pytest tests/ # fast, offline
python -m coverage run -m pytest tests/ ; python -m coverage report proxy_browser_server.py
```
### Live smoke test (AliExpress)
`tests/smoke_aliexpress.py` drives the **real** server against AliExpress — exercising the
full Chrome-impersonation path, retries, byte cap, and HTML→markdown extraction end to end.
Run it from the same env the MCP runs in (needs the runtime deps installed):
```powershell
python tests/smoke_aliexpress.py # "moza racing steering wheel", cheapest-first
python tests/smoke_aliexpress.py "moza r9 wheelbase" # custom query
python tests/smoke_aliexpress.py --raw "moza ks wheel" # raw HTML instead of markdown
```
The same check runs under pytest when opted in:
```powershell
$env:RUN_SMOKE=1 ; python -m pytest tests/test_smoke.py::test_live_aliexpress_moza
```
> AliExpress is heavily JS-rendered and anti-bot defended. The HTTP path gets a real `200` but
> thin/challenge HTML; the [render tier](#headless-browser-render-tier) passes the wall and
> returns the full product grid.
### Live render test (opt-in)
With the render deps installed, exercise the real browser end to end:
```powershell
$env:RUN_RENDER=1 ; python -m pytest tests/test_browser_engine.py::test_live_render_executes_js_and_returns_html
```
## Limitations
- **JavaScript** is handled by the [render tier](#headless-browser-render-tier), **not** the
HTTP engine. Plain `curl_cffi` can't run JS; install the render deps (patchright + Chromium)
and use `render="auto"`/`render="always"` for JS-rendered or challenge-walled sites. Without
the render deps installed, JS-heavy pages come back thin (and are flagged when it's a known
anti-bot wall).
- **Render tier is heavier.** A browser launch costs ~1–3 s and real memory; it only fires on
demand (challenge/thin page in `auto`, or explicit `render`), not on every request.
- **IP reputation still matters.** Even the browser can be IP-flagged after abuse; for the
most aggressive sites pair with a residential `PROXY_URL` and/or `RENDER_HEADLESS=0`.
- **Respect Terms of Service and rate limits.** This tool is for legitimate automation and
research; the built-in per-host throttle, request jitter, and 1–2 request cap keep it polite —
don't hammer sites.
---
## Project layout
```
headless-proxy-mcp/
├── proxy_browser_server.py # the MCP server — HTTP engine, tools, tiered fallback
├── browser_engine.py # headless-browser render engine (patchright/playwright)
├── requirements.txt # runtime dependencies (HTTP engine)
├── requirements-render.txt # optional: headless browser render tier
├── requirements-dev.txt # test dependencies (pytest, coverage)
├── .env.example # configuration reference
├── .gitignore
├── README.md
├── docs/
│ └── anti-bot-bypass.md # playbook: wall tiers, PerimeterX recipe, FB trick
└── tests/
├── conftest.py # stubs curl_cffi/mcp so tests run offline
├── test_proxy_browser.py # hermetic unit tests — HTTP engine + tiered fallback
├── test_browser_engine.py # hermetic unit tests — render engine (mocked browser)
├── smoke_aliexpress.py # live smoke test runner (real network)
└── test_smoke.py # smoke-test unit coverage + opt-in live check
```
## License
MIT (or your preference).
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues