Anthropic Charges You $0.03 per Web Search and Calls It “Included,” and the Browser MCP Crowd Just Let Them
Everyone is arguing about which search API to wire up to Claude Code. Brave, Tavily, Serper, Exa, you, me, all of us, paying fractions of a cent to look up things a human would type into Bing in eight seconds. While that debate runs, your agent quietly bills you every time it has to read a docs page. And the worst part: every one of those services bills you to read pages you are already logged into.
I got tired of it. So I shipped localvily, an open-source local MCP server that gives any AI tool unlimited, session-authenticated web search and page fetch by routing through your own logged-in browser. No API keys. No per-query billing. No rate-limit walls. Paywalled pages, internal docs, anything behind a login cookie, all come back as clean readable text, because the request rides your real browser session.
The package name on PyPI is browser-relay-mcp. The repo is balakumardev/localvily. Run it with one uvx command. I'll show you how, and then I'll explain why this matters more than any of the API-search wars.
The bill nobody is reading
Here is what "web search for agents" actually costs you in 2026, on the metered APIs most people wire up first:
| Service | Pricing model | What you pay for a 1,000-query day |
|---|---|---|
| Brave Search API | $0.50 per 1,000 queries, $3/CPS | ~$0.50 search + scraping on top |
| Tavily | $0.008 per credit, 1 credit per search | ~$8.00 |
| Serper (Google SERP) | $0.30 per 1,000 | ~$0.30, plus the bill for fetching each result yourself |
| Exa | $5 per 1,000 neural searches | ~$5.00 |
| Anthropic WebSearch (Claude tool) | $0.03 per 1k tokens of result text | variable, but not free, and not negotiable |
localvily (browser-relay-mcp) | $0. Flat. | $0, because the tab is yours |
That Anthropic line is the one that should make you angry. Anthropic's own WebSearch tool bills you by the tokens of the result, not the query, so a single agentic loop that reads ten full pages can quietly rack up real money. And the docs page it fetched for you was a page you already have open in your own browser.
How it works, in one diagram
The MCP server is a stdio FastMCP process. On the first tool call it spins up a local FastAPI relay on 127.0.0.1:15552. Two interchangeable drivers sit behind one MCP surface:
┌─────────────────────────────────────────┐
MCP client │ browser-relay-mcp │
(Claude, STORM, …) │ │
│ │ MCP server (stdio / FastMCP) │
│ search/fetch/ │ │ │
│ search_and_ │ │ HTTP │
│ fetch/resume/ │ ▼ │
└───────────────► │ relay (FastAPI @ localhost:15552) │
│ │ │
│ ├── driver=relay ─► Chrome ext ───┤ polls /pending
│ └── driver=cloak ─► patchright ───┤ posts /result
└─────────────────────────────────────────┘
driver=relay(default): a small Chrome extension you load unpacked fromextension/. The extension polls the relay athttp://localhost:15552, runs the search/fetch in a real tab in your logged-in Chrome, and posts the cleaned page text back. Your cookies, your sessions, your IP, your browser fingerprint.driver=cloak: an embedded stealth Chromium launched in-process by patchright. For bot-protected or unattended pages where you do not want to drive your attended Chrome. One-timepatchright install chromium.
Every MCP tool takes a driver parameter, so a caller picks per request.
The five tools, exactly
| Tool | Signature | Returns |
|---|---|---|
search | search(query, k=10, engine="bing", driver="relay") | {status, query, engine, driver, count, results:[{title, url, snippet}]} |
fetch | fetch(url, include_html=False, driver="relay") | {status, url, driver, title, text, excerpt, length[, html]} |
search_and_fetch | search_and_fetch(query, k=5, engine="bing", driver="relay") | batched top-k results, each with text or per-result fetch_error |
resume | resume(resume_token) | the completed result, or action_required again if still blocked |
health | health() | relay + extension + cloak connectivity and queue depth |
search_and_fetch runs the search then fetches the top-k results in parallel. A page that fails records its own fetch_error; the batch still returns. Per-result escalation inside a batch is non-interactive - a blocked result is reported as fetch_error: "action_required: <action>", and you re-drive it with an individual fetch(url).
Install in 60 seconds
You do not need to clone the repo. uvx runs it directly:
uvx browser-relay-mcp
Then add it to your MCP client config:
{ "mcpServers": { "browser-relay": { "command": "uvx", "args": ["browser-relay-mcp"] } } }
For the relay driver, load the extension:
- Open
chrome://extensions, enable Developer mode. - Load unpacked → select the
extension/directory. - Open the extension's options, set the Relay server URL to
http://localhost:15552. - The popup should read Relay OK / connected.
For the cloak driver, one-time:
patchright install chromium
If you only ever use driver: "relay", that step is optional. /health will report the cloak driver as unavailable instead of failing your relay calls.
The escalation flow nobody else does
This is the bit I am proudest of. When a search or fetch hits a CAPTCHA or a login wall, most tools either fail silently or dump the raw HTML on you. localvily pauses the call and surfaces the blocked browser so you can solve it:
search/fetch ──► status: "action_required"
{ driver, action: "solve_captcha" | "login",
message, resume_token, query|url }
│
you solve the CAPTCHA / sign in
in the surfaced browser window
│
▼
resume(resume_token) ──► status: "ok" (completed result)
status: "action_required" (still blocked)
status: "error" (token expired, default TTL 300s)
The resume_token stays valid across repeated rounds (BROWSER_RELAY_ACTION_TTL, default 300s), and resume reuses the held tab instead of starting over. For relay, you solve it in your Chrome. For cloak, the embedded browser is intentionally non-headless so you can see the challenge. Most other tools throw the result away when they hit a wall; this one pauses and waits for you.
A health endpoint you can actually trust
The /health shape is the one I wish every MCP server shipped:
{
"status": "ok",
"extension_connected": true,
"extension_status": "connected",
"last_poll_age_seconds": 2.1,
"search_queued": 0,
"fetch_queued": 0,
"in_flight": 0,
"max_fetch_tabs": 5,
"engine": "bing",
"pending_actions": [],
"version": "0.1.0",
"drivers": {
"relay": { "extension_connected": true, "extension_status": "connected" },
"cloak": { "available": true, "profile_path": "…/browser-relay/cloak-profile" }
}
}
extension_status is one of connected, stale (no poll within ~75s), or never_seen. The cloak block reports available: false with an error when patchright is missing. Your agent can read this and degrade gracefully instead of inventing fake search results.
Real knobs, in real env vars
| Env var | Default | Purpose |
|---|---|---|
BROWSER_RELAY_URL | http://127.0.0.1:15552 | Relay base URL the MCP server talks to |
BROWSER_RELAY_FETCH_CAP | 5 | Max parallel fetch tabs |
BROWSER_RELAY_SEARCH_CONCURRENCY | 1 | Concurrent searches (near-serial by design) |
BROWSER_RELAY_SEARCH_MIN_SPACING_MS | 500 | Minimum spacing between search dispatches |
BROWSER_RELAY_ACTION_TTL | 300 | Seconds a paused (action_required) request stays resumable |
BROWSER_RELAY_DEFAULT_ENGINE | bing | Default search engine |
BROWSER_RELAY_CLOAK_PROFILE_DIR | user cache dir | Persistent profile for the cloak browser |
A STORM adapter, because STORM deserves it
Stanford's STORM knowledge-curation pipeline expects retrievers in the dspy shape. localvily ships adapters/storm.py so the search results drop straight in:
from adapters.storm import to_storm
# `result` is the parsed dict from search_and_fetch(...)
sources = to_storm(result)
# -> [{ "url", "title", "description", "snippets": [chunks of the page text] }, ...]
Only results that actually came back with text are included; page text is chunked at 1000 chars/chunk into snippets.
The acceptance test that matters
The headline test, C3, is 50 sequential searches with 0 errors and 0 silent-empty result sets. That is the actual bar: a search loop that does not break, does not silently hand back empty arrays, and does not lie to the agent. Run it yourself:
cd server && uv run browser-relay-mcp --backend --port 15552
# load extension/, set URL to http://localhost:15552
uv run --with httpx python tests/acceptance/run_acceptance.py
Expected output ends with ALL ACCEPTANCE CRITERIA PASSED.
Why I built this instead of just paying Tavily
I am not anti-Tavily. The team is sharp, the API is fine. But the whole "search API for AI agents" category got priced out. While we all argued about which metered endpoint was cheapest, we forgot the obvious one: the browser tab already open on your screen. Every login you have ever done, every cookie you have ever trusted, every paywall you have ever bypassed as a human, that is your retrieval corpus. localvily is just the relay that lets your agent use it.
The package is browser-relay-mcp. The repo is balakumardev/localvily. MIT-friendly, written in JavaScript and Python, no vendor lock-in, no API keys to lose. Try it on a page that is behind a login and watch your agent stop pretending the internet is anonymous.
Source: https://github.com/balakumardev/localvily