free-search-mcp
A local-first, no-API-key MCP server that enables LLMs to search the web, fetch pages, and read documents using multiple engines and smart fallbacks.
Ask AI about free-search-mcp
Powered by Claude Β· Grounded in docs
I know everything about free-search-mcp. Ask me about installation, configuration, usage, or troubleshooting.
0/500
Reviews
Documentation
free-search-mcp
A local-first, no-API-key Model Context Protocol server that gives any LLM (Claude, GPT, local Ollama, β¦) the ability to search the web, fetch and clean up pages, and read documents β without you signing up for a single search API.
It bundles together the best ideas from a handful of open-source MCPs into one Python package, and adds the LLM-ergonomics and reliability work they were each missing.
research("how does reciprocal rank fusion work", depth=3)
β
# Research brief: how does reciprocal rank fusion work
_engines: duckduckgo, mojeek, searx Β· sources: 3 Β· ~3,400 tokens_
## Sources
- [1] Reciprocal rank fusion | Elasticsearch Reference β <https://β¦>
- [2] Hybrid Search Scoring (RRF) | Microsoft Learn β <https://β¦>
- [3] RRF explained in 4 mins β Medium β <https://β¦>
## Documents
β¦full Markdown bodies of each page, ready for the LLM to readβ¦
One tool call. Three sources. No API key. No OPENAI_API_KEY-but-for-search
shakedown.
Why this exists
Existing search MCPs each do one thing well, but you usually want all of it:
| Multi-engine | No API key | Smart fallback | PDF/DOCX | FTS5 cache | Filters | Trafilatura | LLM-tuned | |
|---|---|---|---|---|---|---|---|---|
nickclyde/duckduckgo-mcp-server | β | β | β | β | β | β | β | ~ |
mrkrsl/web-search-mcp | β | β | β | β | β | β | β | ~ |
Aas-ee/open-webSearch | β | β | ~ | β | β | β | β | ~ |
VincentKaufmann/noapi-google-search-mcp | β | β | β | β | β | β | β | ~ |
| free-search-mcp | β | β | β | β | β | β | β | β |
"LLM-tuned" here means: Markdown-first output, token estimates, smart
truncation at paragraph boundaries, "Best for / Not for / Returns / Common
mistakes" docstrings the model uses to pick the right tool, actionable
error hints, MCP prompts and resource templates, and a one-shot
research() that collapses searchβfetchβfetchβfetch into a single turn.
"Trafilatura" means we extract main content using
trafilatura β winner of the
Bevendorff 2023 ROUGE benchmark (~0.85 vs ~0.55 for naive boilerplate
stripping). Each fetched page also returns author, published_date, and
sitename for free.
"Filters" means search/research accept freshness, include_domains,
exclude_domains, category (news/pdf/github/paper/forum/blog),
include_text, exclude_text.
Anti-detection & resilience
- HTTP fast path uses
curl_cffiwith a real Chrome 131 JA3/JA4 + HTTP/2 fingerprint, fixing the DDG "anomaly 202" rate-limit response that vanilla httpx triggered. - Playwright fallback uses
launch_persistent_context(cookies survive restarts on disk), prefers a real installed Chrome (channel="chrome"), drops the--no-sandboxfingerprint marker on macOS, and randomizes the viewport per session. - Result dedup is title-fuzzy + host-canonical (rapidfuzz
token_set_ratio >= 92, host normalized forwww./m./amp.and country-TLD collapse), catchingbbc.co.ukvsbbc.comduplicates that URL-only dedup misses. searchincludes an honest extractivelead_snippetβ picks the top-3 result whose snippet contains β₯2 query terms and is β₯80 chars; rendered as> **Lead:** According to {host}: β¦. No LLM call. Returns nothing if no snippet qualifies (no fake answer).
β οΈ We deliberately do not attempt to defeat proof-of-work captchas on Bing or Brave β that crosses the ToS line. When those engines challenge us, we fall back to other engines instead.
Tools (9)
| Tool | Description |
|---|---|
search(query, ...filters) | Parallel multi-engine search, RRF-merged, title-fuzzy + host-canonical deduped, with optional extractive lead_snippet |
research(question, depth?, ...filters) | One-shot: search + fetch top N + return Markdown brief |
compare(question, urls=[2..5]) | Concurrent fetch of 2-5 URLs, side-by-side excerpts keyed by question |
fetch(url, render?, ...) | Fetch a page, return reader-mode Markdown (trafilatura, with author/date/sitename) |
fetch_batch(urls, ...) | Concurrent multi-URL fetch |
read_doc(source, start?, length?, ...) | Parse PDF / DOCX / HTML / TXT / MD with pagination |
extract_structured(url, ...) | Pull JSON-LD / OpenGraph / Twitter cards / microdata via extruct |
cache_search(query, limit?, ...) | FTS5 search across previously fetched pages |
engines() | List engine names available to search |
Plus 4 MCP prompts (Research thoroughly, Fact-check claim,
Compare sources, News brief) and 2 resource templates
(cache://page/{url}, cache://search/{query_hash}).
Filters (search / research)
| Param | Values | Effect |
|---|---|---|
freshness | day / week / month / year | Only results from the last N |
include_domains | ["python.org", "djangoproject.com"] | Restrict to these domains |
exclude_domains | ["pinterest.com"] | Remove these |
category | news / pdf / github / paper / forum / blog | Content-type shortcut (paper = arxiv/acm/ieee/β¦, forum = reddit/HN/SE, etc.) |
include_text | "async" | Substring required in title/snippet |
exclude_text | "beginner" | Substring forbidden |
max_age_hours | 24 | Override the 7-day default cache TTL on this call |
All tools default to format="markdown" β readable, ~40% fewer tokens than
JSON, with provenance and a token-budget header. Pass format="json" for
structured access.
Tool annotations
Every tool ships correct readOnlyHint, idempotentHint, and
openWorldHint annotations so MCP clients can label them and gate
elevated actions.
Engines
Default set (all-HTTP, no browser, ~2x faster than the old default
that included Startpage):
duckduckgo, mojeek, googlenews.
Opt-in:
startpageβ browser-rendered (~5-10s/query); good for hard-to-reach results that the HTTP defaults miss.brave,bing,baiduβ intermittent challenges to headless clients.searxβ meta-search proxy via public SearXNG instances; included for completeness but most public instances are slow/unreliable in 2026.
Brave/Bing/Baidu all gate headless browsers after a handful of calls (PoW CAPTCHAs, "something went wrong" pages, redirect wrappers). Pass
engines=["brave"]etc. only when the defaults can't find what you need.
Sparse-result diagnostics
When filters drop results so aggressively that β€3 are returned, the
response includes filter_diagnostics so the LLM knows which knob to
relax. Example for category="forum" + exclude_text="beginner":
β οΈ **Filter diagnostics** (results were sparse)
Raw results: 20 across 3 engines β 0 after filters.
Top drops: category_forum (20).
Hint: Filters dropped 20 of 20 raw results. Most were excluded by
category=forum. Try widening or removing one filter.
Install
git clone https://github.com/ymylive/free-search-mcp.git
cd free-search-mcp
uv sync
uv run playwright install chromium
Run as a stand-alone server (stdio transport):
uv run search-mcp
Run live tests (hits the real web β set the env var):
SEARCH_MCP_TEST_NETWORK=1 uv run pytest -v
Offline tests run by default and don't touch the network.
Wire into Claude Desktop
Add this to ~/Library/Application Support/Claude/claude_desktop_config.json
(macOS) or the equivalent on your platform:
{
"mcpServers": {
"search": {
"command": "uv",
"args": ["--directory", "/absolute/path/to/free-search-mcp", "run", "search-mcp"]
}
}
}
Restart Claude Desktop. The seven tools above will appear in the tool drawer.
Wire into other clients
The server speaks plain MCP over stdio. Anything that supports MCP works:
- Claude Code (
claude mcp add search uv --directory /β¦/free-search-mcp run search-mcp) - Cursor / Continue / Cline (use the JSON snippet above)
- Custom Python / TypeScript clients via the official MCP SDK
Configuration
All settings can be overridden by environment variables prefixed with
SEARCH_MCP_:
| Var | Default | Meaning |
|---|---|---|
SEARCH_MCP_DEFAULT_ENGINES | ["duckduckgo","mojeek","searx"] | JSON list |
SEARCH_MCP_MAX_RESULTS_PER_ENGINE | 10 | |
SEARCH_MCP_RATE_LIMIT_PER_MINUTE | 30 | per engine |
SEARCH_MCP_FETCH_RATE_LIMIT_PER_MINUTE | 20 | shared fetch bucket |
SEARCH_MCP_CACHE_DIR | ~/.cache/search-mcp | |
SEARCH_MCP_CACHE_TTL_SECONDS | 604800 | 7 days |
SEARCH_MCP_FETCH_STRATEGY | auto | auto / http / browser |
SEARCH_MCP_BROWSER_HEADLESS | true | |
SEARCH_MCP_BROWSER_POOL_SIZE | 2 | concurrent pages |
SEARCH_MCP_MAX_CONTENT_CHARS | 50000 | per result truncation |
Architecture
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β FastMCP server (stdio) β
β tools: search / research / fetch / fetch_batch / β
β read_doc / cache_search / engines β
ββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββΌβββββββββββββ ββββββββββββββββββββββββββ
β aggregator β β fetcher β
β - parallel engines β β - httpx fast path β
β - reciprocal rank β β - playwright fallback β
β fusion β β - markdownify β
β - search cache (FTS5) β β - page cache (FTS5) β
ββββββ¬βββββββββββββββββββββ ββββββββββββββ¬ββββββββββββ
β β
ββββββΌββββββββββββββββββ ββββββββββββββββΌββββββββββββββ
β engines/ β β browser pool β
β duckduckgo.py β β - persistent context β
β mojeek.py β β - stealth init script β
β searx.py β β - shared cookies β
β startpage.py (opt) β β - semaphore-bounded pagesβ
β brave.py (opt) β ββββββββββββββββββββββββββββββ
β bing.py (opt) β
β baidu.py (opt) β
ββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββ ββββββββββββββββββββ
β documents/ β β ratelimit β
β pypdf, python-docx, β β token bucket β
β markdownify β β per engine β
ββββββββββββββββββββββββββββββ ββββββββββββββββββββ
ββββββββββββββββββββββββββββββ ββββββββββββββββββββ
β formatting β β research β
β token estimate β β composed β
β smart truncation β β workflow β
β markdown renderers β β β
ββββββββββββββββββββββββββββββ ββββββββββββββββββββ
Engine adapter pattern
Each engine in src/search_mcp/engines/ implements:
class Engine:
name: str
needs_browser: bool # Force Playwright?
wait_selector: str | None # CSS to wait for in browser mode
def build_url(self, query: str, max_results: int) -> str: ...
def parse(self, html: str) -> list[SearchResult]: ...
The base class handles transport (httpx β Playwright fallback), rate limiting, and the case where HTTP returns a captcha shell instead of results (auto-retries via the browser).
Credits
This project stands on the shoulders of:
mrkrsl/web-search-mcpβ smart httpx-then-Playwright fetch strategy, multi-engine fallback chainAas-ee/open-webSearchβ multi-engine breadth (Bing/DDG/Baidu/Brave/Startpage)VincentKaufmann/noapi-google-search-mcpβ anti-detection patterns (navigator.webdriver, UA, cookies), SQLite FTS5 cache idea, multi-formatread_documentnickclyde/duckduckgo-mcp-serverβ per-engine rate limiting, LLM-friendly content cleanup- Mojeek β independent search index that doesn't gate on User-Agent
- Model Context Protocol and the official Python SDK
License
MIT β see LICENSE.
