Skip to content

Commit 8a10c03

Browse files
arham766arham766b
andauthored
mcp: token-cost benchmark in the README + reproducible script (#26)
Adds a Token cost section to mcp/README.md and mcp/benchmark_tokens.py. Handing an agent a raw web page is expensive: the Web scraping Wikipedia article is 68,240 tokens of HTML and Nike's homepage is 353,000 (tiktoken cl100k_base). A built-in web fetch summarizes readable pages cheaply and clears anti-bot on many sites, but returns nothing on JS-rendered pages and some hard walls, where the agent falls back to raw HTML and still fails. The Fortress MCP returns bounded clean output there (285 tokens for the JS quotes page, about 700 for Nike instead of a 403). benchmark_tokens.py counts the tokens each path returns for a list of URLs and flags which ones a naive client is blocked on. Docs plus one script; no engine or server code changed. Co-authored-by: arham766b <arham766b@users.noreply.github.com>
1 parent 3663c95 commit 8a10c03

2 files changed

Lines changed: 115 additions & 0 deletions

File tree

mcp/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,30 @@ gate writes. Every tool is timeout- and SSRF-guarded, caps its output, and retur
117117
structured error instead of hanging. The browser is pre-warmed at startup, so the first
118118
call takes about 100 ms.
119119

120+
## Token cost
121+
122+
Handing an agent a raw web page is expensive. The "Web scraping" Wikipedia article is
123+
68,240 tokens of HTML; Nike's homepage is 353,000 (tiktoken cl100k_base). An agent that
124+
drops a page into context pays that before it reads a word.
125+
126+
A built-in web fetch handles the easy case well. It summarizes readable pages cheaply and
127+
clears anti-bot on many sites. It returns nothing on JavaScript-rendered pages and some hard
128+
walls, and the agent then falls back to the raw HTML and still fails. That gap is where the
129+
Fortress MCP earns its place: one call, bounded clean output, on pages the built-in tool
130+
cannot read. Measured on one residential IP:
131+
132+
| Page | Raw HTML | Built-in web fetch | Fortress MCP |
133+
|---|---|---|---|
134+
| Wikipedia (Web scraping) | 68,240 tok | ~950 tok summary | full clean markdown |
135+
| Nike homepage | 353,241 tok | HTTP 403 | ~700 tok clean text |
136+
| quotes.toscrape.com/js | empty shell | "NO QUOTES FOUND" | 285 tok, all 10 quotes |
137+
| Ticketmaster | 151,946 tok | cleared | cleared |
138+
| Indeed | 403 to a naive client | cleared | cleared |
139+
| Hacker News | 11,765 tok | cleared | cleared |
140+
141+
Reproduce it with [`benchmark_tokens.py`](benchmark_tokens.py): it counts the tokens each path
142+
returns for a list of URLs and flags which ones a naive client is blocked on.
143+
120144
## Benchmarks
121145

122146
The same tasks, run once with an agent's built-in web fetch and again through the Fortress MCP:

mcp/benchmark_tokens.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"""Token cost of giving an AI agent a web page: raw HTML vs the Fortress MCP.
2+
3+
For each URL this prints:
4+
- raw HTML tokens (what a naive agent dumps into context), or the block status
5+
a plain HTTP client hits
6+
- Fortress clean tokens (what the MCP returns), when `tilion` is installed
7+
8+
A built-in web fetch (e.g. Claude Code's WebFetch) sits between these: it summarizes
9+
readable pages cheaply and clears anti-bot on many sites, but it returns nothing on
10+
JavaScript-rendered pages and some hard walls. This script measures the two ends you
11+
can reproduce anywhere; the middle column depends on your client.
12+
13+
Tokens are counted with tiktoken cl100k_base (an approximation for other tokenizers).
14+
15+
pip install tiktoken
16+
pip install "tilion[mcp]" # optional, enables the Fortress column
17+
python benchmark_tokens.py
18+
python benchmark_tokens.py --no-fortress # raw-HTML column only
19+
"""
20+
from __future__ import annotations
21+
import sys
22+
23+
URLS = [
24+
"https://en.wikipedia.org/wiki/Web_scraping",
25+
"https://www.nike.com/",
26+
"https://quotes.toscrape.com/js/",
27+
"https://www.ticketmaster.com/",
28+
"https://news.ycombinator.com/",
29+
]
30+
31+
_UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
32+
"(KHTML, like Gecko) Chrome/125.0 Safari/537.36")
33+
34+
try:
35+
import tiktoken
36+
_enc = tiktoken.get_encoding("cl100k_base")
37+
def ntok(s: str) -> int:
38+
return len(_enc.encode(s))
39+
except Exception:
40+
def ntok(s: str) -> int:
41+
return len(s) // 4 # rough fallback if tiktoken is missing
42+
43+
44+
def raw_html(url: str):
45+
import urllib.request, ssl
46+
ctx = ssl.create_default_context()
47+
ctx.check_hostname = False
48+
ctx.verify_mode = ssl.CERT_NONE
49+
req = urllib.request.Request(url, headers={"User-Agent": _UA})
50+
try:
51+
with urllib.request.urlopen(req, timeout=25, context=ctx) as r:
52+
return r.read().decode("utf-8", "replace"), None
53+
except Exception as e:
54+
return None, type(e).__name__
55+
56+
57+
def fortress_text(url: str):
58+
"""What the Fortress MCP returns for the page. Needs `pip install tilion`."""
59+
try:
60+
import asyncio
61+
from tilion import Tilion
62+
except Exception:
63+
return None, "tilion not installed"
64+
65+
async def run():
66+
async with Tilion(headless=True) as t:
67+
r = await t.fetch(url)
68+
return r.get("text") or r.get("markdown") or ""
69+
70+
try:
71+
return asyncio.run(run()), None
72+
except Exception as e:
73+
return None, type(e).__name__
74+
75+
76+
def main():
77+
want_fortress = "--no-fortress" not in sys.argv
78+
print(f"{'url':46s} {'raw HTML':>18s} {'Fortress MCP':>18s}")
79+
print("-" * 84)
80+
for url in URLS:
81+
html, err = raw_html(url)
82+
raw_col = f"{ntok(html):,} tok" if html else f"blocked ({err})"
83+
f_col = ""
84+
if want_fortress:
85+
text, ferr = fortress_text(url)
86+
f_col = f"{ntok(text):,} tok" if text else (ferr or "-")
87+
print(f"{url:46s} {raw_col:>18s} {f_col:>18s}")
88+
89+
90+
if __name__ == "__main__":
91+
main()

0 commit comments

Comments
 (0)