Skip to content

Commit cb00c41

Browse files
committed
feat(mcp): make fetch stealth by default
fetch now runs the Patchright headless-Chrome stealth path and waits for networkidle, matching the CLI. JS-heavy and anti-bot pages (SPAs, login walls) that returned empty text under plain Playwright now load. Adds a channel arg; raises default timeout to 60s. stealth_fetch stays for finer fingerprint/WebRTC/UA control. Bump 0.5.8 -> 0.5.9.
1 parent 284bb4e commit cb00c41

6 files changed

Lines changed: 51 additions & 26 deletions

File tree

README.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -400,14 +400,15 @@ WebSkrap ships a Model Context Protocol server so MCP clients (Claude Desktop,
400400
Claude Code, Codex, ...) can drive a real browser directly. It exposes three
401401
tools over stdio: `fetch`, `stealth_fetch`, and `doctor`.
402402

403-
Built for LLMs: `fetch` and `stealth_fetch` return **clean visible page text by
404-
default** — no HTML tags, scripts, or style noise — so agents spend tokens on
405-
content, not markup (typically 5-10x fewer tokens than raw HTML). Pass
406-
`text_only=False` when you actually need the HTML. `stealth_fetch` gives agents
407-
the same CDP-leak-free Patchright path the CLI uses, so anti-bot pages that
408-
block naive scrapers still load. Every result carries `status`, `final_url`,
409-
`title`, `text_length`, and truncation flags so the model knows exactly what it
410-
got.
403+
Built for LLMs: both `fetch` and `stealth_fetch` run the same CDP-leak-free
404+
Patchright stealth path the CLI uses (headless Chrome, `networkidle` wait), so
405+
JS-heavy and anti-bot pages that block naive scrapers still load. They return
406+
**clean visible page text by default** — no HTML tags, scripts, or style noise —
407+
so agents spend tokens on content, not markup (typically 5-10x fewer tokens than
408+
raw HTML). Pass `text_only=False` when you actually need the HTML. Use
409+
`stealth_fetch` for finer control (fingerprint surface, WebRTC, UA masking,
410+
persistent profile). Every result carries `status`, `final_url`, `title`,
411+
`text_length`, and truncation flags so the model knows exactly what it got.
411412

412413
```bash
413414
pip install webskrap

SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,8 @@ webskrap-mcp
143143

144144
MCP tools:
145145

146-
- `fetch`: standard Playwright fetch.
147-
- `stealth_fetch`: Patchright fetch.
146+
- `fetch`: Patchright stealth fetch (headless Chrome, waits for networkidle).
147+
- `stealth_fetch`: stealth fetch with finer fingerprint/WebRTC/UA controls.
148148
- `doctor`: Playwright/Chromium MCP readiness check.
149149

150150
## Validation

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "webskrap"
7-
version = "0.5.8"
7+
version = "0.5.9"
88
description = "A Playwright-based Python scraping framework with coherent browser profiles and session controls."
99
readme = "README.md"
1010
requires-python = ">=3.11"

src/webskrap/mcp_server.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,27 +32,37 @@
3232
async def fetch(
3333
url: str,
3434
profile: str = "desktop-chrome",
35-
wait_until: str = "domcontentloaded",
35+
channel: str = "chrome",
36+
wait_until: str = "networkidle",
3637
resource_policy: str = "all",
37-
timeout_ms: float = 30_000,
38+
timeout_ms: float = 60_000,
3839
max_chars: int = 20_000,
3940
text_only: bool = True,
4041
) -> dict[str, Any]:
41-
"""Fetch a URL with a standard Playwright browser and return page data.
42+
"""Fetch a URL with the Patchright stealth driver and return page data.
4243
43-
Returns clean visible page text by default (LLM-friendly, no HTML tags).
44-
Set text_only=False to get the raw HTML instead.
44+
Uses the same CDP-leak-free headless-Chrome stealth path as the CLI, so
45+
JS-heavy and anti-bot pages that block naive scrapers still load. Waits for
46+
networkidle by default so single-page apps have hydrated before reading.
47+
Returns clean visible page text by default (LLM-friendly, no HTML tags); set
48+
text_only=False to get the raw HTML instead. For finer stealth control
49+
(fingerprint surface, WebRTC, UA masking, persistent profile) use
50+
stealth_fetch.
4551
4652
Args:
4753
url: The URL to load.
4854
profile: Bundled profile (desktop-chrome, desktop-edge, mobile-chrome).
55+
channel: Browser channel, e.g. chrome. Use chromium on Linux ARM64.
4956
wait_until: commit, domcontentloaded, load, or networkidle.
5057
resource_policy: all, lite (block images/fonts/media), or documents.
5158
timeout_ms: Navigation timeout in milliseconds.
5259
max_chars: Maximum characters of page text to return.
5360
text_only: Return clean visible text (default) instead of raw HTML.
5461
"""
5562
config = SessionConfig(
63+
driver="patchright",
64+
channel=channel,
65+
headless=True,
5666
navigation_timeout_ms=timeout_ms,
5767
resource_policy=parse_resource_policy(resource_policy),
5868
)

tests/test_mcp.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,18 @@ def test_fetch_defaults_to_clean_text(monkeypatch: Any) -> None:
4646
assert result["text"] == "Readable body"
4747

4848

49+
def test_fetch_uses_stealth_driver_by_default(monkeypatch: Any) -> None:
50+
_fake_client(monkeypatch)
51+
52+
asyncio.run(mcp_server.fetch("https://example.test"))
53+
54+
config = _FakeClient.calls[0]["config"]
55+
assert config.driver == "patchright"
56+
assert config.channel == "chrome"
57+
assert config.headless is True
58+
assert _FakeClient.calls[0]["wait_until"] == "networkidle"
59+
60+
4961
def test_fetch_text_only_false_returns_html(monkeypatch: Any) -> None:
5062
_fake_client(monkeypatch)
5163

web/content/user-guide/mcp.md

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ WebSkrap ships a [Model Context Protocol](https://modelcontextprotocol.io)
44
server. MCP clients such as Claude Desktop, Claude Code, and Codex can call it to
55
drive a real browser directly. It runs over stdio and exposes three tools.
66

7-
**Built for LLMs.** `fetch` and `stealth_fetch` return clean visible page text
8-
by default — no HTML tags, scripts, or CSS noise — so the model spends tokens on
9-
content, not markup (typically 5-10x fewer tokens than raw HTML). `stealth_fetch`
10-
gives agents the same CDP-leak-free Patchright path the CLI uses, so anti-bot
11-
pages that block naive scrapers still load. Pass `text_only=false` when you
12-
actually need the HTML.
7+
**Built for LLMs.** Both `fetch` and `stealth_fetch` run the same CDP-leak-free
8+
Patchright stealth path the CLI uses (headless Chrome, `networkidle` wait), so
9+
JS-heavy and anti-bot pages that block naive scrapers still load. They return
10+
clean visible page text by default — no HTML tags, scripts, or CSS noise — so
11+
the model spends tokens on content, not markup (typically 5-10x fewer tokens
12+
than raw HTML). Use `stealth_fetch` for finer fingerprint/WebRTC/UA control.
13+
Pass `text_only=false` when you actually need the HTML.
1314

1415
## Install
1516

@@ -34,8 +35,8 @@ python -m webskrap.mcp_server
3435

3536
| Tool | Purpose |
3637
| --- | --- |
37-
| `fetch` | Fetch a URL with a standard Playwright browser. |
38-
| `stealth_fetch` | Fetch a URL with the Patchright stealth driver. |
38+
| `fetch` | Fetch a URL with the Patchright stealth driver (waits for `networkidle`). |
39+
| `stealth_fetch` | Same stealth driver with finer fingerprint/WebRTC/UA controls. |
3940
| `doctor` | Check that Playwright and Chromium can launch. |
4041

4142
Both fetch tools return `status`, `final_url`, `title`, `ok`, `headers`, and the
@@ -51,9 +52,10 @@ text; set `text_only` to `false` to get raw HTML.
5152
| --- | --- | --- |
5253
| `url` | required | URL to load. |
5354
| `profile` | `desktop-chrome` | Bundled profile name. |
54-
| `wait_until` | `domcontentloaded` | `commit`, `domcontentloaded`, `load`, or `networkidle`. |
55+
| `channel` | `chrome` | Browser channel; use `chromium` on Linux ARM64. |
56+
| `wait_until` | `networkidle` | `commit`, `domcontentloaded`, `load`, or `networkidle`. |
5557
| `resource_policy` | `all` | `all`, `lite`, or `documents`. |
56-
| `timeout_ms` | `30000` | Navigation timeout. |
58+
| `timeout_ms` | `60000` | Navigation timeout. |
5759
| `max_chars` | `20000` | Maximum returned text characters. |
5860
| `text_only` | `true` | Return clean visible text; set `false` for raw HTML. |
5961

0 commit comments

Comments
 (0)