searxng-mcp-scraper
{
"answer": "searxng-mcp-scraper is an MCP server that gives AI agents web search and content extraction superpowers — all powered by a self-hosted SearXNG metasearch instance with no API keys or third-party tracking required.\n\n### Tools & Capabilities\n\n- search — Run web searches through SearXNG with full control over categories, engines, language, page number, time range, safesearch, and result count. Returns ranked results with title, URL, snippet, engine source, and score.\n\n- fetch — Fetch any public HTTP(S) URL and extract cleaned main text via trafilatura, stripping navigation, scripts, and boilerplate. Includes automatic Firecrawl fallback for JavaScript-rendered pages when static fetch comes up empty, and built-in SSRF protection that blocks private/loopback network targets.\n\n- scrape_blog — Discover every post on a blog (RSS/Atom feed first, HTML index fallback), fetch each post's cleaned text in parallel, and aggregate everything into a single Markdown file with YAML front matter. Failures are noted in-band rather than aborting.\n\n- deep_scrape — Everything scrape_blog does, plus: discovers linked documents (PDF, DOCX, CSV, EPUB, etc.) and images, downloads assets locally, extracts text via MarkItDown, and embeds it all into the output Markdown. Supports PDF text extraction with scanned/OCR classification.\n\n### Deployment\n\nSupports two MCP transports — stdio (Claude Desktop, Cursor) and streamable-http (Open WebUI, browser clients) — with pre-configured CORS. Timeouts, concurrency, size limits, and Firecrawl settings are all tunable via environment variables."
}
Provides search and fetch tools using a self-hosted SearXNG instance, enabling web searches and extraction of clean text from web pages.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@searxng-mcp-scraper search for open source MCP servers on GitHub"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
searxng-mcp-scraper
MCP server exposing SearXNG search and HTTP fetch tools for AI agents.
A self-contained Model Context Protocol server that turns SearXNG (a self-hosted metasearch engine) into two MCP tools you can wire into Claude Desktop, Cursor, or any MCP-compatible client:
search(query, ...)— ranked web results from SearXNG's JSON APIfetch(url)— cleaned main text of any public http(s) URL
No API keys, no third-party tracking, no rate limits beyond what your SearXNG instance imposes.
Installation
# From source (recommended during dev)
git clone https://github.com/your-org/searxng-mcp-scraper
cd searxng-mcp-scraper
uv sync --extra dev
uv pip install -e .
# Or with plain pip
pip install -e .Related MCP server: searxng-mcp
Configuration
The server reads configuration from environment variables (or a .env file at startup):
Variable | Required | Default | Description |
| yes | — | Base URL of your SearXNG instance, e.g. |
| no |
| Default search category |
| no |
| Default engine list when a caller doesn't pass |
| no |
| Default language code |
| no |
| Default safesearch: |
| no |
| Hard cap on results per call |
| no |
| HTTP timeout (seconds) for search |
| no |
| HTTP timeout (seconds) for fetch |
| no |
| Refuse to read more than this many bytes per fetch |
| no |
| Max HTTP redirects to follow |
| no |
| Allow fetching private/loopback URLs (SSRF guard, default on) |
| no |
| User-Agent header for all requests |
| no |
| Stderr log verbosity: |
| no |
| Bind host when |
| no |
| Bind port when |
| no |
| Comma-separated CORS allow-origins. Use |
| no |
| Directory for |
| no |
| Max post pages per blog scrape |
| no |
| Max parallel post fetches |
| no |
| Max HTML index pages to scan when no feed is found |
| no |
| Max linked documents downloaded/extracted by one |
| no |
| Refuse a single linked document above this size |
| no |
| Max images downloaded by one |
| no |
| Refuse a single image above this size |
| no |
| Max parallel document/image downloads during |
| no |
| Retry a failed/thin static fetch through Firecrawl (see below) |
| no |
| Firecrawl API base — the local keyless proxy by default |
| no | — | Optional. Empty uses the keyless free tier |
| no |
| Timeout for a fallback scrape (Firecrawl renders before responding) |
| no |
| Static extractions shorter than this are treated as failures and retried |
Note: your SearXNG instance must have
jsonenabled insearch.formatsundersettings.yml, orsearchwill returnsearxng_unavailablewith a 403.
PDF extraction (optional, local, keyless)
deep_scrape downloads linked PDFs and extracts their text. By default it
uses markitdown. For a much faster and cleaner text extraction on
text-based PDFs, install the optional extra:
uv sync --extra pdf-extras # or: pip install "searxng-mcp-scraper[pdf-extras]"This pulls in pdf-inspector —
Firecrawl's open-source Rust PDF engine (no API key, no cloud, runs
locally). When installed, PDF extraction routes through it first:
text-based PDFs are read straight from the PDF internals (fonts, text operators) in milliseconds.
scanned / image-only PDFs are correctly classified as needing OCR (the library reports
pdf_type: scanned). It has no OCR of its own, so those fall through to markitdown (which may have an OCR backend); if markitdown also returns nothing, the document surfaces an honestDocumentExtractionFailedinstead of a silent empty string.
The dependency is optional: without it, PDFs simply use markitdown. Nothing in the core package depends on a native wheel.
Firecrawl fallback (JS-rendered pages)
fetch does a plain HTTP GET and runs trafilatura over the HTML. That is fast and
free, but it returns nothing useful for client-rendered pages: the served body is an
empty root div, so trafilatura scrapes up the meta description and a nav label and
returns a couple hundred characters of noise. Measured on real sites:
URL | static | with fallback |
| 208 chars | 15,972 |
| 156 chars | 7,737 |
| 326 chars | 1,126 |
| 57 chars | 655 |
| 2,764 chars | 2,764 (static kept — already good) |
When a fetch errors, returns empty text, or returns fewer than
FIRECRAWL_MIN_TEXT_CHARS, the URL is retried once through Firecrawl's
/v2/scrape, which renders the page server-side and returns clean markdown.
Results carry extractor: "firecrawl" so you can tell which path produced them.
Firecrawl's hosted API has a keyless free tier for /v2/scrape and /v2/search,
so this costs nothing and needs no account. The catch: the tier rejects any
Authorization header, while most clients insist on sending one. FIRECRAWL_BASE_URL
therefore points at a small local proxy that strips the header:
python3 scripts/firecrawl_keyless_proxy.py # 127.0.0.1:8788The proxy is vendored here (no external dependency) and forwards a real key
if you set FIRECRAWL_UPSTREAM_KEY in its environment — which unlocks
/v2/map, /v2/crawl, and /v2/extract through the same URL. Key-gated
paths without a key get a clear error rather than a confusing upstream 403.
Set FIRECRAWL_API_KEY and point FIRECRAWL_BASE_URL at https://api.firecrawl.dev
to skip the proxy and use a real account instead.
The fallback never overrides a guard. invalid_url and private_network_blocked
are SSRF/scheme decisions, not extraction failures — routing those through a
third-party renderer would defeat the guard, so they are excluded. oversize is
excluded too (re-rendering the same huge page just burns the cap again). If the
fallback fails or returns less text than the static path, the original result is
kept unchanged. Set FIRECRAWL_FALLBACK_ENABLED=false for fully air-gapped runs.
Running
The server supports two transports. Pick the one your MCP client speaks:
stdio (default — Claude Desktop, Cursor, etc.)
# From a checkout, with .env in the cwd
SEARXNG_BASE_URL=http://localhost:8888 searxng-mcp-scraper
# Or via uv
SEARXNG_BASE_URL=http://localhost:8888 uv run searxng-mcp-scraper
# Or as a module
SEARXNG_BASE_URL=http://localhost:8888 python -m searxng_mcp_scraperThe server speaks MCP over stdio. It writes logs to stderr; stdout is reserved for the JSON-RPC stream and must not be polluted.
streamable-http (llama-ui, Open WebUI, browser-based clients)
SEARXNG_BASE_URL=http://localhost:8888 uv run searxng-mcp-scraper --transport streamable-httpThis binds to http://127.0.0.1:8765/mcp by default. Override with MCP_HOST / MCP_PORT
env vars. CORS is pre-configured for http://localhost:* and http://127.0.0.1:* — any
browser on your machine can connect.
If you need to bind to 0.0.0.0 (LAN), set MCP_CORS_ORIGINS to a narrower list. There is
no auth in v1 — do not expose this to the public internet without putting it behind a
reverse proxy with auth.
llama-ui / Open WebUI config
For browser-based MCP clients, run the server in HTTP mode and paste the URL into the client's "MCP server URL" field:
In one terminal:
SEARXNG_BASE_URL=http://localhost:8888 uv run searxng-mcp-scraper --transport streamable-httpIn the UI, add a new MCP server with URL
http://127.0.0.1:8765/mcp(no auth headers required for local).The
searchandfetchtools will appear in the model's tool list.
Claude Desktop config
Add this to ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"searxng-scraper": {
"command": "searxng-mcp-scraper",
"env": {
"SEARXNG_BASE_URL": "http://localhost:8888"
}
}
}
}If you installed from source and the binary isn't on your PATH, point command at uv:
{
"mcpServers": {
"searxng-scraper": {
"command": "uv",
"args": ["--directory", "/absolute/path/to/searxng-mcp-scraper", "run", "searxng-mcp-scraper"],
"env": {
"SEARXNG_BASE_URL": "http://localhost:8888"
}
}
}
}Restart Claude Desktop; you should see search and fetch tools appear.
Tool reference
search(query, ...) -> {results, suggestions, number_of_results}
Parameter | Type | Default | Description |
| string | (required) | Search query string |
| list[string] |
| SearXNG categories, e.g. |
| list[string] |
| Restrict to specific engines, e.g. |
| string |
| Language code |
| int |
| Page number (1-indexed) |
|
|
| Time filter |
|
|
| Safe-search level |
| int |
| Cap on results returned (1–50) |
fetch(url) -> {url, final_url, content_type, text, byte_count}
Parameter | Type | Description |
| string | An |
Strips <script>, <style>, <nav>, <header>, <footer>, <aside> blocks via
trafilatura, and collapses whitespace. Refuses non-http(s) schemes and (by default) private
network targets. Returns a structured error on timeout, oversize, or non-2xx — never crashes.
scrape_blog(blog_url) -> {output_path, post_count, ...}
Discovers a blog's RSS/Atom feed first, falls back to scanning HTML index pages, fetches post
text, and writes one Markdown file under BLOG_SCRAPE_OUTPUT_DIR.
deep_scrape(blog_url) -> {output_path, assets_dir, documents_*, images_*}
Does everything scrape_blog does, plus:
discovers linked documents from each post (
.pdf,.docx,.csv,.md, etc.)downloads raw document files into
deep_assets/extracts document text with MarkItDown and embeds it in the Markdown output
discovers images from
<img src>, lazy-load attributes,srcset, and direct image linksdownloads bounded image files into
deep_assets/images/records per-image URL, saved path, content type, byte count, and any failure in the Markdown
The returned summary includes images_found, images_saved, and image_byte_count. One bad
image/document is recorded in-band and does not fail the whole blog scrape.
Development
uv run pytest # run the full test suite
uv run mypy src # static type checks
uv run ruff check # lintLicense
MIT.
Available Tools
4 toolsdeep_scrapeA
Discover every post on a blog, fetch each, AND download + extract every linked document (PDF/DOCX/XLSX/PPTX/CSV/JSON/XML/MD/EPUB/...).
Same discovery as `scrape_blog` (RSS/Atom first, HTML index
fallback). For every post fetched, the raw HTML is scanned for
links whose URL ends in a recognized document extension. Each
such document is downloaded (capped by
`deep_scrape_max_documents_per_blog`, 100 by default) and its
text is extracted via markitdown. The original blog post text
and every document's extracted text are all inlined into one
Markdown file at:
{blog_scrape_output_dir}/<safe-host>_<safe-path>_<unix-ts>_deep.md
Downloaded binaries are saved alongside, under:
{blog_scrape_output_dir}/deep_assets/
Per-post and per-document failures are noted in-band as
`- Fetch error: <code>` / `- Error: <code>` bullets, not by
aborting the whole run. The returned summary dict only carries
an `error` field when discovery itself failed (no posts).
Args:
blog_url: Root URL of the blog (e.g. "https://blog.example.com/").
Returns:
Small summary dict — never the file contents:
{blog_url, output_path, assets_dir, post_count, post_count_ok,
documents_found, documents_extracted, post_byte_count,
document_byte_count, duration_s, discovery}
On failure: {blog_url, error: <stable_code>, message}.
Inputs: blog_url (required), ctx. Returns: object
fetchA
Fetch a URL and return its cleaned main text content.
Args:
url: An http:// or https:// URL.
Returns:
Dict with `url`, `final_url`, `content_type`, `text`, `byte_count`,
and on error an `error` field with a stable string code.
Inputs: url (required), ctx. Returns: object
scrape_blogA
Discover every post on a blog and write a single aggregated Markdown file.
Tries the blog's RSS/Atom feed first (common paths like /rss.xml,
/feed, /atom.xml). If no feed responds, crawls the HTML index
(up to blog_scrape_discovery_pages pages). Each discovered post
URL is fetched in parallel and its cleaned main text is appended
to one Markdown file at:
{blog_scrape_output_dir}/<safe-host>_<safe-path>_<unix-ts>.md
The file starts with YAML front matter (blog URL, post count,
byte total, duration) followed by one H3 section per post with
its title, original URL, published date (if known from the feed),
content-type, byte count, and the cleaned text. Failures are
noted in-band as `- Fetch error: <code>` bullets, not by aborting
the whole run.
Args:
blog_url: Root URL of the blog (e.g. "https://blog.example.com/").
Returns:
Small summary dict — never the file contents:
{blog_url, output_path, post_count, post_count_ok, byte_count,
duration_s, discovery}
On failure: {blog_url, error: <stable_code>, message}.
Inputs: blog_url (required), ctx. Returns: object
searchA
Search the web via the configured SearXNG instance.
Args:
query: The search query string. Required.
categories: SearXNG category list, e.g. ["general"], ["images"].
Defaults to the server's `SEARXNG_DEFAULT_CATEGORY`.
engines: Restrict to specific engines, e.g. ["google", "bing"].
language: Language code, e.g. "en", "de". Defaults to server setting.
pageno: Page number (1-indexed). Default 1.
time_range: One of "day", "month", "year", or None.
safesearch: 0 (none), 1 (moderate), 2 (strict). Defaults to server setting.
max_results: Cap on results returned (1-50). Default 10.
Returns:
Dict with `results` (list of {title, url, snippet, engine, score}),
`suggestions`, `number_of_results`, and on error an `error` field.
Inputs: query (required), ctx, pageno, engines, +5 more. Returns: object
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 4 tools
The tools are mostly distinct: search performs queries, fetch retrieves a single URL, while scrape_blog and deep_scrape both focus on blog content aggregation. The overlap between scrape_blog and deep_scrape is clear (deep_scrape is an extended version), but both descriptions are detailed enough to avoid confusion in practice.
The names mix conventions: 'search' and 'fetch' are single generic verbs, 'scrape_blog' follows a verb_noun pattern, and 'deep_scrape' is an adjective_verb compound. While each name is readable and descriptive, the lack of a uniform pattern makes the set feel slightly inconsistent.
With only 4 tools, the server is well-scoped for its purpose. Each tool covers a distinct aspect of web scraping and search: querying, single-page extraction, blog aggregation, and deep scraping with document extraction. No tool feels redundant or unnecessary.
The tool surface covers the core workflows: web search, URL fetching, blog scraping, and extended scraping with document extraction. Minor gaps exist, such as a generic crawling tool for non-blog sites, but these are not critical given the server's stated focus on search and blog content.
Maintenance
Related MCP Connectors
Web search API and MCP server for developers building AI agents.
Docs: https://docs.keenable.ai/mcp-server Keenable is a free, remote MCP server that gives agents access to the web index. Search the web with ranked results and date/site filters, then fetch any indexed page as clean markdown. Works out of the box with no account or API key.
One MCP server for all web data. Scrape, search, crawl, and AI research in one install.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceA lightweight MCP server that enables LLMs to search the web via DuckDuckGo, search GitHub code repositories, and extract clean content from web pages in LLM-friendly formats.9MIT
- FlicenseAqualityDmaintenanceMCP server that provides a search_web tool to query a self-hosted SearXNG instance and return structured web search results.1-
- FlicenseNot gradedqualityBmaintenanceA self-hosted MCP server providing private web search, web page fetching, and current date/time tools, powered by a bundled SearXNG instance for API-key-free local search.2-
- AlicenseNot gradedqualityDmaintenanceMCP server that provides web search scraping from DuckDuckGo (with Mojeek fallback) and URL content fetching as markdown/text or raw HTML.1ISC