Skip to content

Commit 43d3c18

Browse files
committed
feat(mcp): return clean text by default for LLMs
fetch and stealth_fetch now expose text_only (default True), reusing the existing client.fetch(text_only=...) path. Agents get clean visible page text instead of raw HTML, ~5-10x fewer tokens. Pass text_only=False for HTML. README and site copy lead with the LLM/agent story. Bump 0.5.7 -> 0.5.8.
1 parent edab4ee commit 43d3c18

6 files changed

Lines changed: 116 additions & 22 deletions

File tree

README.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
<h1 align="center">WebSkrap</h1>
66

77
<p align="center">
8-
<strong>Async-first Python scraping framework built on Playwright.</strong><br>
9-
<em>It provides coherent browser profiles, persistent sessions, resource routing, and Patchright-powered stealth for data collection workflows that need realistic browser behavior.</em>
8+
<strong>Async-first Python scraping framework built on Playwright — and a first-class web tool for LLMs and agents.</strong><br>
9+
<em>Coherent browser profiles, persistent sessions, resource routing, and Patchright-powered stealth for data collection workflows that need realistic browser behavior. Ships an MCP server so Claude, Codex, and any MCP agent can fetch live pages as clean, token-efficient text.</em>
1010
</p>
1111

1212
WebSkrap does not include CAPTCHA solving, login-wall bypassing, credential bypassing, or access-control circumvention. Use it only on targets you are allowed to access.
@@ -396,9 +396,18 @@ stderr is not a TTY.
396396

397397
## MCP server
398398

399-
WebSkrap ships an optional Model Context Protocol server so MCP clients (Claude
400-
Desktop, Claude Code, ...) can drive scraping directly. It exposes three tools
401-
over stdio: `fetch`, `stealth_fetch`, and `doctor`.
399+
WebSkrap ships a Model Context Protocol server so MCP clients (Claude Desktop,
400+
Claude Code, Codex, ...) can drive a real browser directly. It exposes three
401+
tools over stdio: `fetch`, `stealth_fetch`, and `doctor`.
402+
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.
402411

403412
```bash
404413
pip install webskrap

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.7"
7+
version = "0.5.8"
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 & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,21 @@ async def fetch(
3636
resource_policy: str = "all",
3737
timeout_ms: float = 30_000,
3838
max_chars: int = 20_000,
39+
text_only: bool = True,
3940
) -> dict[str, Any]:
4041
"""Fetch a URL with a standard Playwright browser and return page data.
4142
43+
Returns clean visible page text by default (LLM-friendly, no HTML tags).
44+
Set text_only=False to get the raw HTML instead.
45+
4246
Args:
4347
url: The URL to load.
4448
profile: Bundled profile (desktop-chrome, desktop-edge, mobile-chrome).
4549
wait_until: commit, domcontentloaded, load, or networkidle.
4650
resource_policy: all, lite (block images/fonts/media), or documents.
4751
timeout_ms: Navigation timeout in milliseconds.
48-
max_chars: Maximum characters of page HTML to return.
52+
max_chars: Maximum characters of page text to return.
53+
text_only: Return clean visible text (default) instead of raw HTML.
4954
"""
5055
config = SessionConfig(
5156
navigation_timeout_ms=timeout_ms,
@@ -58,6 +63,7 @@ async def fetch(
5863
config=config,
5964
wait_until=parse_wait_until(wait_until),
6065
timeout_ms=timeout_ms,
66+
text_only=text_only,
6167
)
6268
return shape_fetch_result(result, max_chars)
6369

@@ -75,11 +81,14 @@ async def stealth_fetch(
7581
webrtc_ip_handling_policy: str | None = None,
7682
timeout_ms: float = 90_000,
7783
max_chars: int = 20_000,
84+
text_only: bool = True,
7885
) -> dict[str, Any]:
7986
"""Fetch a URL with the Patchright stealth driver (CDP-leak-free).
8087
81-
Requires Patchright's browser download: webskrap install. Prefer
82-
headless=False with channel="chrome" for the strictest anti-bot path.
88+
Returns clean visible page text by default (LLM-friendly, no HTML tags).
89+
Set text_only=False to get the raw HTML instead. Requires Patchright's
90+
browser download: webskrap install. Prefer headless=False with
91+
channel="chrome" for the strictest anti-bot path.
8392
8493
Args:
8594
url: The URL to load.
@@ -93,7 +102,8 @@ async def stealth_fetch(
93102
webrtc_ip_handling_policy: Chromium WebRTC ICE policy, e.g.
94103
disable_non_proxied_udp.
95104
timeout_ms: Navigation timeout in milliseconds.
96-
max_chars: Maximum characters of page HTML to return.
105+
max_chars: Maximum characters of page text to return.
106+
text_only: Return clean visible text (default) instead of raw HTML.
97107
"""
98108
config = SessionConfig(
99109
driver="patchright",
@@ -112,6 +122,7 @@ async def stealth_fetch(
112122
profile=get_profile(profile),
113123
config=config,
114124
timeout_ms=timeout_ms,
125+
text_only=text_only,
115126
)
116127
return shape_fetch_result(result, max_chars)
117128

tests/test_mcp.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from __future__ import annotations
2+
3+
import asyncio
4+
from typing import Any
5+
6+
from webskrap import mcp_server
7+
from webskrap.models import FetchResult
8+
9+
10+
class _FakeClient:
11+
calls: list[dict[str, Any]] = []
12+
13+
async def __aenter__(self) -> _FakeClient:
14+
return self
15+
16+
async def __aexit__(self, exc_type: object, exc: object, traceback: object) -> None:
17+
return None
18+
19+
async def fetch(self, url: str, **kwargs: Any) -> FetchResult:
20+
self.calls.append({"url": url, **kwargs})
21+
text = "Readable body" if kwargs.get("text_only") else "<html>abcdef</html>"
22+
return FetchResult(
23+
url=url,
24+
final_url=f"{url}/final",
25+
status=200,
26+
ok=True,
27+
headers={"content-type": "text/html"},
28+
text=text,
29+
title="Example",
30+
cookies=[],
31+
timings={"elapsed_ms": 12.34},
32+
)
33+
34+
35+
def _fake_client(monkeypatch: Any) -> None:
36+
_FakeClient.calls = []
37+
monkeypatch.setattr(mcp_server, "WebSkrapClient", _FakeClient)
38+
39+
40+
def test_fetch_defaults_to_clean_text(monkeypatch: Any) -> None:
41+
_fake_client(monkeypatch)
42+
43+
result = asyncio.run(mcp_server.fetch("https://example.test"))
44+
45+
assert _FakeClient.calls[0]["text_only"] is True
46+
assert result["text"] == "Readable body"
47+
48+
49+
def test_fetch_text_only_false_returns_html(monkeypatch: Any) -> None:
50+
_fake_client(monkeypatch)
51+
52+
result = asyncio.run(mcp_server.fetch("https://example.test", text_only=False))
53+
54+
assert _FakeClient.calls[0]["text_only"] is False
55+
assert result["text"] == "<html>abcdef</html>"
56+
57+
58+
def test_stealth_fetch_defaults_to_clean_text(monkeypatch: Any) -> None:
59+
_fake_client(monkeypatch)
60+
61+
result = asyncio.run(mcp_server.stealth_fetch("https://example.test"))
62+
63+
assert _FakeClient.calls[0]["text_only"] is True
64+
assert result["text"] == "Readable body"

web/content/user-guide/mcp.md

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
# MCP server
22

3-
WebSkrap ships an optional [Model Context Protocol](https://modelcontextprotocol.io)
4-
server. MCP clients such as Claude Desktop and Claude Code can call it to drive
5-
scraping directly. It runs over stdio and exposes three tools.
3+
WebSkrap ships a [Model Context Protocol](https://modelcontextprotocol.io)
4+
server. MCP clients such as Claude Desktop, Claude Code, and Codex can call it to
5+
drive a real browser directly. It runs over stdio and exposes three tools.
6+
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.
613

714
## Install
815

@@ -32,8 +39,9 @@ python -m webskrap.mcp_server
3239
| `doctor` | Check that Playwright and Chromium can launch. |
3340

3441
Both fetch tools return `status`, `final_url`, `title`, `ok`, `headers`, and the
35-
page HTML in `text` (capped by `max_chars`, with `text_length` and
36-
`text_truncated` reporting the full size).
42+
page content in `text` (capped by `max_chars`, with `text_length` and
43+
`text_truncated` reporting the full size). By default `text` is clean visible
44+
text; set `text_only` to `false` to get raw HTML.
3745

3846
## Tool arguments
3947

@@ -46,7 +54,8 @@ page HTML in `text` (capped by `max_chars`, with `text_length` and
4654
| `wait_until` | `domcontentloaded` | `commit`, `domcontentloaded`, `load`, or `networkidle`. |
4755
| `resource_policy` | `all` | `all`, `lite`, or `documents`. |
4856
| `timeout_ms` | `30000` | Navigation timeout. |
49-
| `max_chars` | `20000` | Maximum returned HTML characters. |
57+
| `max_chars` | `20000` | Maximum returned text characters. |
58+
| `text_only` | `true` | Return clean visible text; set `false` for raw HTML. |
5059

5160
Example arguments:
5261

web/src/app/(site)/page.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,12 @@ const FEATURES = [
4545
{
4646
title: "MCP server",
4747
description:
48-
"Expose fetch and stealth_fetch tools to agents without extra Python packages.",
48+
"fetch and stealth_fetch tools for Claude, Codex, and any MCP agent — returning clean text, not tag soup.",
4949
},
5050
{
51-
title: "LLM-friendly CLI",
51+
title: "Built for LLMs",
5252
description:
53-
"Use JSON, bounded output, stdout, and text-only fetches from the terminal.",
53+
"Clean-text output by default, 5-10x fewer tokens than raw HTML, plus JSON and bounded CLI output.",
5454
},
5555
];
5656

@@ -112,9 +112,10 @@ export default function Home() {
112112
Scrape the web like a real browser
113113
</h1>
114114
<p className="mt-6 max-w-2xl text-lg text-muted-foreground">
115-
WebSkrap is an async-first Python scraping framework built on Playwright. Coherent
116-
browser profiles, persistent sessions, resource routing, Patchright-powered stealth, and
117-
machine-readable CLI output for data collection that needs realistic behavior.
115+
WebSkrap is an async-first Python scraping framework built on Playwright — and a
116+
first-class web tool for LLMs and agents. Coherent browser profiles, persistent sessions,
117+
Patchright-powered stealth, and an MCP server that hands agents live pages as clean,
118+
token-efficient text.
118119
</p>
119120
<div className="mt-10 flex flex-col gap-3 sm:flex-row">
120121
<Button size="xl" render={<Link href={DOCS_URL} />}>

0 commit comments

Comments
 (0)