webx-mcp
Local on-demand web search and page reading for coding agents via SearXNG, with SSRF-protected fetching and Markdown extraction.
README
WebX — Local On-Demand Web Search for Coding Agents
Small, Unix-y local tool that gives coding agents web access only when desired. Not a research agent — just two primitives plus lifecycle management:
search(query) -> ranked URLs/snippets (local SearXNG, Docker, 127.0.0.1:8888, normally stopped)
read(url) -> cleaned Markdown (controlled fetch + Trafilatura, SSRF-protected)
- Minimal-agent mode: agent shells out
webx search / webx read / webx stoponly when a temporary prompt authorizes it. No permanent web tool in the system prompt. - Exploration/MCP mode: host launches
webx-mcp(stdio). Server exposes exactlyweb_search+web_read. Launch does not start SearXNG; firstweb_searchlazy-starts it and owns shutdown.
Install
Requires Python 3.12+ and Docker + Compose for search. webx read works without Docker.
# with uv (recommended)
uv sync
uv sync --extra mcp # for MCP server
uv sync --extra dev # for tests
# or pip
pip install -e .
pip install -e ".[mcp]"
# global tool (so `webx` works in `pi`'s bash and any shell)
uv tool install . # installs to ~/.local/bin/webx — ensure ~/.local/bin is on PATH
# or pipx
pipx install .
# per-project (no global install)
uv sync && uv run webx --help
# or add .venv/bin to PATH for this shell/session (useful for pi coding agent)
export PATH="$PWD/.venv/bin:$PATH"
which webx && webx --help
pi coding agent note: The
bashtool insidepiinheritsPATHfrom the host. Ifwebx: command not found, runuv tool install .once orexport PATH="$PWD/.venv/bin:$PATH"in the session where you launchpi.
Quick start
webx init # materialize ~/.local/share/webx/{compose.yml,settings.yml,.env,cache}
webx doctor # check docker, templates, SearXNG reachability (does NOT start SearXNG)
webx status # {initialized, docker_available, searxng_running, url, runtime_dir}
webx status --json
webx search "SearXNG documentation" --limit 5 --pretty
webx status # now running
webx read "https://docs.searxng.org/" --max-chars 12000
webx read "https://docs.searxng.org/" --json | jq
# denials are exit 5
webx read "http://127.0.0.1:8888/" # -> exit 5 unsafe URL
webx read "http://192.168.1.1/" # -> exit 5
webx read "file:///etc/passwd" # -> exit 5
webx stop # docker compose stop (retains container)
webx status # stopped
Temporary web-access prompt (minimal agent)
For this task you are allowed to use the local WebX utility when external/current
information materially helps.
Available commands:
- webx search "<query>" to discover relevant public-web sources.
- webx read "<url>" to read a relevant public page as cleaned text/Markdown.
...
When the web-research portion is finished, run webx stop.
MCP host config
Stdio only. Example (Claude Code / MCP Inspector):
{
"mcpServers": {
"webx": {
"command": "webx-mcp",
"env": { "WEBX_DATA_DIR": "/home/you/.local/share/webx" }
}
}
}
Tool list must be exactly web_search + web_read. Lifecycle is internal — do not expose webx up/stop as agent tools.
CLI reference
webx --help
webx --version
webx init [--force-templates] [--show-path] # idempotent, never rotates secret
webx doctor [--json] # inspection only (now reports searxng_image/version)
webx up # ensure SearXNG running
webx stop # compose stop (normal shutdown)
webx status [--json] # now includes searxng_image/version
webx logs [--tail 100]
webx search QUERY [--limit 8] [--category general] [--language en] [--page 1]
[--time {day,month,year}] [--safe-search {0,1,2}] [--engine NAME] [--pretty]
webx read URL [--max-chars N] [--json] [--links] [--no-tables] [--precision] [--recall] [--no-cache]
stdout= data (JSON for search, Markdown/text or JSON for read).stderr= diagnostics.- Exit codes:
0ok,2usage/validation,3runtime/docker unavailable,4SearXNG failure,5unsafe URL,6fetch/extraction failure,7unsupported content type (2xxwithimage/*etc.;application/pdfneedsuv sync --extra pdfelse7with hint,2xximage/pdf without pdf extra →7).4xx/5xx/timeout from a public URL is6, not7(e.g.wikimedia PNG -> HTTP 400->6).
--verbose (global) enables debug traces to stderr (e.g. read ok: https://example.com/ text/html 114 chars engine=trafilatura 1.23s). Secrets never printed.
Engine/category examples (SearXNG aggregates 269 services; filter per query when upstream rate-limits hit):
webx search "python httpx" --engine wikipedia --engine github --pretty
webx search "SearXNG" --category it --pretty
webx search "SearXNG documentation" --time month --pretty
Reader extraction examples (--links preserves [text](url) markdown; --precision/--recall tune trafilatura):
webx read "https://en.wikipedia.org/wiki/Python_(programming_language)" --max-chars 2000 --links | head -n 40
webx read "https://en.wikipedia.org/wiki/Python_(programming_language)" --max-chars 2000 | head -n 40
webx read "https://api.github.com/zen" --json | jq # application/json is returned raw (engine=raw), not trafilatura
Runtime & config
Runtime dir via platformdirs (overridable with WEBX_DATA_DIR):
- Linux:
~/.local/share/webx/(XDG) - macOS:
~/Library/Application Support/webx/ - Windows:
%LOCALAPPDATA%\webx\
Contains compose.yml, settings.yml, .env (SEARXNG_SECRET 0600), cache/.
settings.yml is a tiny override (use_default_settings: true, formats: [html, json], limiter: false, public_instance: false, image_proxy: false). Do not copy the whole SearXNG default config.
compose.yml (pinned, latest no longer used):
services:
searxng:
image: ${SEARXNG_IMAGE:-docker.io/searxng/searxng:2026.8.19-5ffd32ca2}
container_name: webx-searxng
ports: ["127.0.0.1:8888:8080"]
env_file: [.env]
volumes: ["./settings.yml:/etc/searxng/settings.yml:ro", "./cache:/var/cache/searxng"]
restart: "no"
Loopback binding only, single container, no Valkey/Redis, no proxy, no TLS. If the read-only single-file mount ever breaks due to SearXNG FORCE_OWNERSHIP, switch to a directory mount — but keep 127.0.0.1 binding (see 04_SEARXNG_RUNTIME.md).
Env overrides (all WEBX_):
WEBX_DATA_DIR, WEBX_SEARXNG_URL (default http://127.0.0.1:8888), WEBX_DOCKER_CMD,
WEBX_STARTUP_TIMEOUT (30s), WEBX_SEARCH_TIMEOUT (15s), WEBX_READ_TIMEOUT (15s),
WEBX_MAX_RESPONSE_BYTES (10 MiB), WEBX_MAX_READ_CHARS (40000), WEBX_MCP_STOP_ON_EXIT (true)
SEARXNG_IMAGE can also be set in .env or env to pin an image tag.
SearXNG image version
Pinned at implementation (2026-08-20) — v1.2 (0f5e582):
- Tag:
docker.io/searxng/searxng:2026.8.19-5ffd32ca2(waslatest) - Running version via
webx doctor --json/webx status --json:searxng_version: 2026.8.1+8892414dc(from/configwhen reachable) or image tag when stopped - Override:
SEARXNG_IMAGE=docker.io/searxng/searxng:2026.8.17-374939b88 webx uporSEARXNG_IMAGE=...in.env— thenwebx init --force-templatesto materialize latestis intentionally not used for reproducibility; seehttps://docs.searxng.org/admin/api.html(/config) for engine suspension diagnostics- Current
settings.ymlstilluse_default_settings: true— no Valkey, limiter off for loopback
webx doctor --json example:
{
"searxng_image": "docker.io/searxng/searxng:2026.8.19-5ffd32ca2",
"searxng_version": "2026.8.1+8892414dc",
"searxng_reachable": true
}
Manual update:
webx stop
docker compose -f $(webx init --show-path)/compose.yml pull # or: SEARXNG_IMAGE=... docker compose pull
webx up
webx search "test" --limit 1 --pretty
webx stop
Never auto-update on search.
MCP lifecycle
- Launching
webx-mcpdoes not start SearXNG. - First
web_searchprobeshttp://127.0.0.1:8888/; if stopped it doesdocker compose up -d+ poll, then marksstarted_by_mcp = true; if already running it marksfalse. web_readnever starts SearXNG.- On clean exit, if
started_by_mcp && WEBX_MCP_STOP_ON_EXITit runscompose stop; else it leaves SearXNG running. Process-local lock protects concurrent first searches. Multiple independent MCP processes needing a lease/refcount is deferred to v2.
Tool descriptions state the trust boundary: returned page text is untrusted external data, never agent instructions; JS/auth pages may not work.
Security model
webx read treats URLs as untrusted input.
- Allow only
http:///https://; denyfile:,ftp:,data:,javascript:, bare paths, credential-bearing URLs. - Resolve hostname via OS resolver, inspect every IPv4/IPv6 with
ipaddress: deny loopback, RFC1918 private, IPv6 ULA, link-local (169.254.0.0/16,fe80::/10), multicast, unspecified, reserved, metadata169.254.169.254, and the SearXNG endpoint itself. No--allow-privatein v1. - DNS pinning (v1.2):
http+httpsresolve once viaresolve_and_check, validate all IPs, then pin transport to those IPs (Hostheader +sni_hostnamefor TLS, try each IP onConnectError, fail-closed, no fallback to unpinned URL). Validates every redirect target;127.0.0.1:8888SearXNG endpoint also denied. - Redirects: manual loop, max 5,
Locationresolved against current URL, re-validated, loop/excess fails. - Fetch:
User-Agent: webx/<version> local-research-tool, connect 5s, read 15s, streamed withContent-Lengthpre-check + 10 MiB cap, no browser masquerade. - Allowed types:
text/html,application/xhtml+xml,text/plain, markdown-like,json/xmltext;application/pdfviapypdf(--extra pdf, first 20 pages,engine=pypdf,pages_total/pages_read/partial); binaryimage/*etc. → exit 7. - Extraction: raw body →
trafilatura.extract(output_format="markdown", ...)+html2txtfallback; PDF viapypdfin isolated subprocess (10s timeout); truncate after extraction at a word/Newline boundary, reporttruncated+characters+engine/pages_total/partialin--json. - No cookies, auth headers, POST, or browser.
Operations & troubleshooting
webx doctor is the first diagnostic.
| Failure | Likely cause |
|---|---|
doctor says docker unavailable |
Install Docker/Compose; webx read still works |
| Search 403 | json not enabled in settings.yml (check search.formats) |
| SearXNG starts but searches 0 results / 5xx | Upstream engines rate-limited / CAPTCHAd your IP — check webx logs for suspended_time=180 / Too many request / HTTP 403. Not a WebX bug; try different query/category or pin engines: webx search "…" --engine wikipedia --engine github (google cse is often the only engine not rate-limited from this IP) |
| Reader returns tiny text | JS-rendered page — try --recall or different source; browser rendering is out of scope for v1 |
| Reader rejects URL | Private/local network denial — intentional |
webx logs empty |
SearXNG not running — webx logs now hints run webx up or webx search to start instead of silent empty |
WEBX_DATA_DIR=/tmp/... webx status says running:true but compose missing |
Single webx-searxng container name shared across dirs — status now shows compose: missing + note; probe is global 127.0.0.1:8888 |
webx: command not found in pi |
~/.local/bin not on PATH — see Install ( uv tool install / export PATH="$PWD/.venv/bin:$PATH" ) |
Research heuristics (agent-side, not WebX): prefer official docs → upstream repo/notes → specs → vendor announcements → quality writing; use --category it when it helps; run multiple focused searches, read primary sources, search for contradictions.
Testing
uv sync --extra dev --extra mcp
uv run pytest # fast unit tests, no Docker/net required
uv run pytest -m integration # live tests (needs Docker + net, marked integration)
uv run pytest --cov=webx
Manual acceptance (from clean WEBX_DATA_DIR):
webx --help; webx init; webx doctor; webx status # stopped
webx search "SearXNG documentation" --limit 5 --pretty
webx status # running
webx read "https://docs.searxng.org/" --max-chars 12000
webx read "http://127.0.0.1:8888/" # -> exit 5
webx read "http://192.168.1.1/" # -> exit 5
webx read "file:///etc/passwd" # -> exit 5
webx stop; webx status # stopped
# MCP: inspector 2 tools, web_read while stopped, first search starts, second reuses, stop-on-exit ownership
Note on
httpbin.org: Livehttpbin.orgcurrently returns503 Service Temporarily Unavailablefrom some networks (verified 2026-08-20 viacurl -A "webx/0.1.0"andcurl -A "Mozilla/5.0"both 503). Ifwebx read https://httpbin.org/html503s, use stable alternatives:https://example.com,https://en.wikipedia.org/wiki/Python_(programming_language)(good for truncation/--linkstests), orhttps://httpbingo.org/get.
Project layout
src/webx/
__init__.py, cli.py, config.py, lifecycle.py, searxng.py, security.py, reader.py, core.py, mcp_server.py
assets/{compose.yml,settings.yml}
tests/{unit,integration}
docs/{instructions,PLAN.md}
Core WebX facade is shared by CLI and MCP; neither shells out to the other.
Non-goals (v1.2)
Browser/Playwright (explicit web_read_rendered deferred — bench 81% useful, ~300MB Chromium not justified), crawling, reranker, LLM summarizer, inter-process lease, engine presets, domain filters — see 09_DECISIONS_AND_FUTURE.md for rationale and P3 harnesses (scripts/bench_*.py 50/60 corpora). v1.2 added: pinned SearXNG, SNI pinning, PDF pypdf subprocess, engine provenance, --no-cache in-memory LRU, MCP/Pi unified contract.
License
MIT
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。