Query-driven web search + page fetch for AI agents. The agent gives it a natural-language question, gets back ranked results with snippets (and optionally full page content) ready to cite.
A small MCP server with two tools that compose cleanly:
| Tool | Takes | Returns |
|---|---|---|
web_search(query, ...) |
A search query in plain English | Ranked results from real search engines, with title, URL, snippet, and (optionally) full extracted page content |
get_page_content(url, ...) |
A URL the agent already knows | Cleaned main text of that page |
Multi-engine fallback: Bing via headless Chromium → DuckDuckGo HTML via httpx. No API keys required.
This is the shape you want when the agent is answering a question like "what's the latest model context protocol spec?" or "find a good tutorial on asyncio". The agent doesn't need to know which engine to use, what URL to fetch, or how to render the SERP — it just calls web_search and gets back ready-to-inject context.
git clone https://github.com/ptrken01/web-browser-mcp
cd web-browser-mcp
uv venv .venv --python 3.11
uv pip install --python .venv/bin/python -e ".[dev]"
.venv/bin/playwright install chromium(If you have an existing venv from elsewhere, just uv pip install -e . in it
and run playwright install chromium once. The .[dev] extra adds pytest,
mypy, and ruff.)
.venv/bin/web-browser-mcpIn your client's MCP config:
{
"mcpServers": {
"web-search": {
"command": "/absolute/path/to/web-browser-mcp/.venv/bin/web-browser-mcp"
}
}
}.venv/bin/web-browser-mcp --transport streamable-http
# Default: http://127.0.0.1:8766/mcpIn llama-ui's MCP server settings, add an HTTP transport pointing at
http://127.0.0.1:8766/mcp. CORS is open for localhost / 127.0.0.1 by
default.
Give it a natural-language question or topic. Returns ranked results with title, URL, and snippet, ready to be cited in your answer.
{
"name": "web_search",
"arguments": {
"query": "model context protocol specification",
"limit": 5,
"include_content": false
}
}Parameters:
| Param | Type | Default | Notes |
|---|---|---|---|
query |
str |
— | Required. Natural-language search query (1-2000 chars). |
limit |
int |
5 |
Max results (1-10). |
include_content |
bool |
False |
When True, follows each result URL and extracts the main text of the page. Adds latency. |
engine_order |
list[str] |
["bing", "duckduckgo"] |
Override the engine priority. Subset of ["bing", "duckduckgo"]. |
timeout_s |
float |
15 |
Per-engine timeout in seconds. |
Response:
{
"query": "model context protocol specification",
"engine": "duckduckgo",
"count": 5,
"results": [
{
"title": "Official site",
"url": "https://modelcontextprotocol.io",
"snippet": "Model Context Protocol",
"engine": "duckduckgo"
},
{
"title": "What is the Model Context Protocol (MCP)?",
"url": "https://modelcontextprotocol.io/docs/getting-started/intro",
"snippet": "MCP (Model Context Protocol) is an open-source standard for connecting AI applications to external systems...",
"engine": "duckduckgo"
}
],
"duration_s": 1.234
}When include_content=True, each result additionally has:
{
"content": "Main text extracted from the page...",
"content_chars": 3421
}For the "I have a specific URL and want its content" case.
{
"name": "get_page_content",
"arguments": {
"url": "https://example.com/article",
"max_chars": 5000
}
}Response:
{
"url": "https://example.com/article",
"final_url": "https://example.com/article",
"title": "Example Article",
"text": "Main content of the page...",
"text_chars": 3421,
"duration_s": 0.5
}The tool tries engines in engine_order. The first one to return ≥1 result
wins. If all engines fail, the response has an error field with a stable
string code.
| Engine | How | Strengths | Weaknesses |
|---|---|---|---|
| Bing | Playwright headless Chromium | Full SERP, rich snippets, related questions | Can hit captcha on shared IPs; slower |
| DuckDuckGo HTML | httpx (no JS) | Reliable, fast, no browser overhead | Sometimes rate-limited under heavy use |
In practice, DDG HTML is the workhorse — it's the engine that succeeds most often in test runs. Bing is the upgrade path for richer SERP data when the agent has a fresh IP and the captcha doesn't trip.
All errors come back as structured error fields, not exceptions:
| Error code | When |
|---|---|
invalid_query |
Empty query or query too long (>2000 chars). |
invalid_url |
get_page_content got a non-http URL or URL with no host. |
search_engine_error |
The active engine returned an error. Try a different engine_order. |
search_timeout |
The active engine timed out. |
browser_not_initialized |
An engine that needs Playwright was called without a browser. |
fetch_failed |
get_page_content got a non-2xx response or connection error. |
extraction_failed |
get_page_content got a response but trafilatura couldn't extract text. |
lifespan did not initialize |
Server lifespan never ran — see the searxng-mcp-scraper pitfall. Should not happen with this server. |
| Env var | Default | Notes |
|---|---|---|
HEADLESS |
True |
Set to False to see the browser. |
BROWSER |
chromium |
chromium / firefox / webkit. |
BROWSER_TIMEOUT_S |
30.0 |
Default per-step timeout. |
NAVIGATION_TIMEOUT_S |
30.0 |
Default web_search engine timeout. |
USER_AGENT |
(Chrome UA) | Override if you get blocked. |
LOG_LEVEL |
INFO |
DEBUG / INFO / WARNING / ERROR. |
MCP_HOST |
127.0.0.1 |
Bind host (streamable-http transport). |
MCP_PORT |
8766 |
Bind port (streamable-http transport). |
MCP_CORS_ORIGINS |
localhost,127.0.0.1 |
Comma-separated CORS allow-list. |
.venv/bin/python -m pytest14 tests pass. The end-to-end test (tests/test_server_e2e.py) boots
real uvicorn + real FastMCP + a real Playwright browser, performs the MCP
initialize handshake, and calls web_search against real search
engines. This is the test that catches lifespan-bug classes from
searxng-mcp-scraper — unit tests that call tool functions directly with
hand-built state would miss them.
| Tool | Source | When to use |
|---|---|---|
web-browser-mcp.web_search |
Bing / DDG (this repo) | Agent has a question, needs relevant web results. |
web-browser-mcp.get_page_content |
httpx + trafilatura | Agent has a specific URL, wants its content. |
searxng-mcp-scraper.search |
SearXNG metasearch | When you want to control the engines, categories, language. |
searxng-mcp-scraper.fetch |
trafilatura over HTTP | Fast static-HTML extraction. |
searxng-mcp-scraper.scrape_blog |
RSS + parallel fetch | Whole blog → one .md. |
searxng-mcp-scraper.deep_scrape |
scrape_blog + docs | Blog + linked PDFs / docs. |
Use web_search first for general questions. Drop to get_page_content or
searxng-mcp-scraper.fetch when you have a URL. Use scrape_blog for
"read this whole blog" use cases.
MIT
All products + free articles: autoincomesys.com — local-first AI dev notes, build-once digital products (Gumroad).