Skip to main content
Glama
README.md
# local-web — local web search / fetch MCP for Claude Code

A thin local MCP server (FastMCP, HTTP transport) over three components:

| Component | Role | Runs |
|---|---|---|
| **SearXNG** | metasearch (Google/Bing/DuckDuckGo/Scholar/…), JSON API | `127.0.0.1:8888` (own venv, `searxng/`) |
| **Crawl4AI** | JS-rendered page → LLM markdown (Playwright Chromium) | inside the MCP server process |
| **PyMuPDF 2.x** | PDF → markdown | inside the MCP server process |
| **arXiv / alphaxiv / GitHub / HuggingFace APIs** | dedicated citation & code lookups | no local component |

Everything is managed with **uv**. The MCP project (this folder) is a uv project
(`pyproject.toml` + `uv.lock`); SearXNG is a git checkout with a uv-managed venv.

## Tools

- `web_search(query, num_results=8, engine="auto", searx_engines="", time_range="")`
  — general web search (SearXNG: Google/Bing/…), automatic DuckDuckGo (`ddgs`)
  fallback. Pin engines with `searx_engines="google"`. `time_range`:
  day|week|month|year.
- `scholar_search(query, num_results=8)` — Google Scholar via SearXNG
  (`engines=google scholar`); no offline fallback (SearXNG must be running).
- `github_search(query, kind="repo"|"issue"|"code", num_results=8)` — GitHub REST
  API. Code search needs `GITHUB_TOKEN` env var; unauthenticated limit is
  10 search req/min.
- `huggingface_search(query, kind="model"|"dataset", num_results=8, pipeline="")`
  — HF API. `query` matches **name/ID substrings** (e.g. `"wan"`), not full
  text; `pipeline` filters by task tag (e.g. `"video-classification"`).
- `libgen_search(query, num_results=8)` — scrapes libgen.li's `index.php?req=`
  results table (best-effort; the site changes and may block). Returns title,
  authors, publisher, year, language, pages, size, extension, md5, plus
  `item_url` (edition page) and `file_url` (file page with download mirrors).
- `arxiv_search(query, max_results=5)` — arXiv API entries (id, title, authors,
  date, abstract, abs URL; PDF = same URL with `.pdf`). Retries up to 3× with
  backoff on transient timeouts. arXiv policy: ≤1 req/3s, one connection at a
  time; the server spaces arxiv.org requests 5s apart and, on a 429 ("Rate
  exceeded"), pauses the whole domain ~5 min for all agents instead of retrying
  into the burst counter.
- `alphaxiv_lookup(arxiv_id, lang="en")` — AI overview of an arXiv paper
  (blurb/problem/solution/key-insights/results, the machine-readable report,
  abstract, bibtex, cited papers). Bare ID (`2402.02085`) or URL. For a short
  blurb this is the fastest path. `fetch_page`/`fetch_pdf` of arXiv paper URLs
  already route through alphaxiv first, so you rarely need to call these
  directly — use them when you want the structured overview rather than raw text.
- `alphaxiv_fulltext(arxiv_id, offset=0, max_chars=20000)` — full extracted
  paper text as markdown, paginated like `fetch_page`. For equations/tables/
  section detail the lookup card doesn't have.
- `fetch_page(url, offset=0, max_chars=20000)` — rendered markdown, **paginated**:
  call again with `offset = offset + max_chars` while `offset < total_chars`.
  Browser concurrency is capped (semaphore, `BROWSER_SLOTS`, default 4).
  **arXiv paper URLs (abs/pdf) are routed through alphaxiv first** — the result's
  `source` field says which answered (`alphaxiv` | `browser` | `arxiv-direct`).
- `fetch_stealth(url, offset=0, max_chars=20000, solve_cloudflare=False)` —
  Scrapling `StealthyFetcher` (stealth-patched headless Chromium: real browser
  UA, canvas noise, WebRTC blocking, spoofed locale/timezone). Use when
  `fetch_page` gets blocked or returns a challenge page.
  `solve_cloudflare=True` solves Cloudflare Turnstile/interstitials (slow,
  1–3 min when it has to). Paginated the same way.
- `fetch_pdf(url, offset=0, max_chars=20000)` — PDF → markdown (PyMuPDF 2.x),
  paginated the same way. For arXiv paper PDFs the default route is alphaxiv's
  extracted text; the direct download happens only when alphaxiv lacks the paper.

## Run / registration (two modes)

| Name | Transport | Lifecycle |
|---|---|---|
| `local-web` | **stdio — the default** | Claude Code spawns `uv run python server.py` per session (like the chrome-devtools plugin). Nothing to start, nothing to keep alive. |
| `local-web-shared` | HTTP :9000 | One long-lived server shared by all sessions (shared rate-limit gate + browser pool). Auto-starts at logon (scheduled task `local-web-mcp-stack` → `autostart.vbs`) and self-heals at every session start (SessionStart hook → `check-and-start.bat`). |

Both modes also use SearXNG on :8888 for `web_search`/`scholar_search`
(`web_search` falls back to DuckDuckGo if SearXNG is down). `check-and-start.bat`
is idempotent: it starts only whatever port is not listening.

Manual launch (visible windows): `start-all.bat` / `./start-all.sh`, or
individually `start-searxng.*` / `start-server.*` (the server launchers set
`MCP_TRANSPORT=http`).

Re-register from scratch (both at user scope):

```
claude mcp add -s user local-web -- <path to uv> run --directory <this folder> python server.py
claude mcp add -s user local-web-shared --transport http http://127.0.0.1:9000/mcp
claude mcp list        # both should show Connected
```

Note: with both registered, a session sees the 11 tools twice (once per
server prefix). Use `local-web` (stdio) for everyday work; the shared one is
for running several sessions/agents concurrently against one browser pool.

## Tests

```
uv run pytest                          # unit tests: pure helpers + domain gate
                                       # (fake clock, no network, ~1s)
uv run python smoke_test.py            # live end-to-end, all 11 tools
SKIP_ARXIV=1 uv run python smoke_test.py   # skip arXiv while it's rate-limiting
```

Unit tests run in CI (`.github/workflows/ci.yml`); the smoke test hits the
live HTTP server (start it with `check-and-start.bat` or `start-all.bat`),
so it's local-only.

## (Re)setup from scratch

```powershell
# MCP project (this folder)
uv add fastmcp crawl4ai httpx pymupdf beautifulsoup4 ddgs "scrapling[fetchers]" markdownify
uv run python -m playwright install chromium    # Crawl4AI + Scrapling browsers
uv run scrapling install                        # marks Scrapling's browser deps installed

# SearXNG (searxng/ — clone of github.com/searxng/searxng at depth 1)
uv venv searxng/.venv --python 3.11
uv pip install --python searxng/.venv -r searxng/requirements.txt
```

SearXNG user settings live in `searxng/searx/settings/user-settings.yml`:
`search.formats` includes `json` (default is html-only → JSON returns 403) and
`server.limiter: false`. The dev runner generates no storage dependency; if you
see botdetection/limiter errors, keep `limiter: false` (there is no valkey/redis).

Note on cloning SearXNG on Windows: the repo contains a file whose name includes
a colon (`utils/templates/etc/httpd/sites-available/searxng.conf:socket`), which
Windows cannot check out. Clone with `--no-checkout` and restore only what is
needed: `git checkout HEAD -- searx requirements.txt`.

## Security

- The MCP server binds to **127.0.0.1 only and has no auth layer** — do not
  expose it (no tailscale/0.0.0.0 binding).
- Fetched web content is **untrusted data** (prompt-injection vector). Keep
  side-effecting tools (shell, file writes) out of any agent that browses.
- Search/fetch hit the public internet normally; be considerate (the semaphore
  and Crawl4AI's cache mitigate re-hammering).

## Troubleshooting

- `web_search` errors mention `searxng: …` → SearXNG not running (start it).
  `engine` in the result shows which backend answered (`searxng` or `ddg`).
- SearXNG JSON 403 → `json` missing from `search.formats` in user-settings.yml.
- `fetch_page` browser errors → `uv run python -m playwright install chromium`.
- Crawl4AI setup check: `uv run python -c "import crawl4ai_doctor"` or
  `uv run crawl4ai-doctor` if the CLI is exposed by the installed version.
- Ports in use: SearXNG 8888, MCP 9000 (`SEARXNG_URL` env var overrides the
  SearXNG address in `server.py`).
- `arxiv_search` returns a 429 error: the arXiv burst counter is active (can
  last ~15 min). Don't retry in a loop — that refreshes the window. Fetch the
  specific papers via `alphaxiv_lookup`/`alphaxiv_fulltext` meanwhile.
  `SKIP_ARXIV=1 uv run python smoke_test.py` skips the arXiv case during a
  throttle window.

TDQS

A3.7/5.0

Scored across 11 tools

Disambiguation4/5

The search tools are cleanly separated by domain (web, scholar, arXiv, GitHub, HF, libgen), and fetch_page/fetch_stealth/fetch_pdf have described fallback roles. There is still minor overlap between fetch_page, alphaxiv_fulltext, and fetch_pdf for retrieving arXiv/PDF content, but the descriptions mostly resolve when to use each.

Naming Consistency4/5

All names are snake_case and readable, with fetch_* for retrieval and *_search for domain searches. The mix of verb-first (fetch_page), domain-first (arxiv_search), and prefixed tools (alphaxiv_lookup) is a minor inconsistency rather than a confusing one.

Tool Count5/5

Eleven tools are well within a reasonable scope for a web research server. Each search domain and fetch variant covers a distinct source or retrieval mode, so the set does not feel bloated.

Completeness4/5

The surface covers general web search, academic/arXiv search, code/model/book search, plus page, PDF, stealth, and arXiv full-text fetching. Minor gaps remain around crawling/site mapping or generic file extraction, but core research workflows are covered.

Maintenance

ActivityMaintained
ResponsivenessNo issues