free-search-mcp
A keyless, local-first MCP server for searching the web, reading and comparing pages and documents, walking citation graphs, and querying a local page cache — all without API keys.
search— parallel multi-engine web search (DuckDuckGo, Bing, Yahoo! JAPAN, …), RRF-merged and deduplicated, with filters for freshness, domains, text, and source category (papers, finance, CVE, weather, software, etc.).research— one-shot search + fetch of the top N results into a citable Markdown brief.fetch— read a URL as reader-mode Markdown, parsed document text, or a resource description;inline=Truereturns images for vision models.fetch_batch— concurrently fetch up to 20 URLs in one call.read_doc— parse and paginate PDF, DOCX, XLSX, PPTX, EPUB, CSV, code, archives, and (optionally sandboxed) local files.paper_graph— follow a paper's references and citing works, with Crossref retraction/correction notices, from a DOI, OpenAlex ID, arXiv ref, or title.compare— fetch 2–5 URLs concurrently and return side-by-side excerpts keyed to one question.extract_structured— pull JSON-LD, OpenGraph, Twitter cards, and microdata (prices, authors, dates, etc.) from a page.cache_search— FTS5 full-text search over pages already fetched locally.engines— list the available source tree (group → sub-group → engine) to choose engines or categories.download— save a file to disk in an auto-expiring downloads directory.
Included in the 'paper' category filter, restricting search results to arXiv (along with other academic domains).
Allows web search using Baidu as an opt-in engine, but may face intermittent challenges for headless clients.
Allows web search using Brave Search as an opt-in engine, though may require browser fallback to handle captchas.
Allows web search using DuckDuckGo, one of the default multi-engine search sources without API key.
Allows web search using Mojeek, one of the default multi-engine search sources without API key.
Allows web search using Startpage, one of the default multi-engine search sources without API key.
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., "@free-search-mcp research Quantum computing breakthroughs, depth=2"
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.
free-search-mcp
https://github.com/user-attachments/assets/354b522a-ef6b-4547-bb07-c80c6268a5e8
free-search-mcp is a local-first Model Context Protocol server that needs no API key. It lets any LLM (Claude, GPT, a local Ollama model, …) search the web, fetch and clean up pages, and read documents, and you never sign up for a search API.
It combines ideas from several open-source MCP servers in one Python package, and adds the output shaping for LLMs and the reliability work that each of them lacked.
research("how does reciprocal rank fusion work", depth=3)
↓
# Research brief: how does reciprocal rank fusion work
_engines: duckduckgo, bing, yahoojp, anysearch · 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…That was one tool call. It returned three sources with their full text, and no API key was involved.
Quick start
You need uv and nothing else: no sign-up, no API key, no clone.
In Claude Code, inside a session:
/plugin marketplace add sweetcornna/free-search-mcp
/plugin install free-search@free-search-mcpIn Codex:
codex plugin marketplace add sweetcornna/free-search-mcp
codex plugin add free-search@free-search-mcp(In Codex, a [mcp_servers.search] entry left in ~/.codex/config.toml by an
earlier codex mcp add silently shadows the plugin's server. Remove it with
codex mcp remove search.)
The plugin brings the 11 tools, the verified-research skill (how to check a
snippet against its page and its date) and, in Claude Code, the
free-search:quick-search agent, all pinned to one version.
/plugin update free-search moves to the next release.
Optionally, install Chromium once for the browser-rendered engines (brave,
zhihu, …) and JavaScript-heavy pages. Everything else works
without it, and a call that needs it returns this command as its error:
uvx --from free-search-mcp playwright install chromiumClaude Desktop, other clients, a source checkout and Docker are under Install.
Related MCP server: uvxwebsearchmcp
Why this exists
Multi-engine | No API key | Smart fallback | PDF/DOCX | FTS5 cache | Filters | Trafilatura | LLM-tuned | |
| ✗ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ~ |
| ✓ | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ~ |
| ✓ | ✓ | ~ | ✗ | ✗ | ✗ | ✗ | ~ |
| ✗ | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ~ |
free-search-mcp | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
"LLM-tuned" means Markdown-first output with token estimates, errors that name
the next step, and a research() that turns a search and its fetches into one
call. docs/HOW_IT_WORKS.md explains the less obvious
columns.
Tools (11)
Tool | Description |
| Parallel multi-engine search, RRF-merged, title-fuzzy + host-canonical deduped, with optional extractive |
| One-shot: search + fetch top N + return Markdown brief |
| Walk one paper's citation graph: references, citing works ranked by influence, and Crossref retraction/correction notices. Takes a DOI, an OpenAlex ID, an exact title, or an arXiv reference ( |
| Concurrent fetch of 2-5 URLs, side-by-side excerpts keyed by question |
| Fetch any resource: reader-mode Markdown for pages, parsed text for documents, or a description (type/size/dimensions/sha256) for images and binaries. |
| Concurrent multi-URL fetch (max 20 per call) |
| Parse PDF / DOCX / XLSX / PPTX / EPUB / CSV / code / zip-tar / HTML / TXT / MD with pagination |
| Pull JSON-LD / OpenGraph / Twitter cards / microdata via extruct. Long prose fields ( |
| FTS5 search across previously fetched pages |
| The source tree (group, then sub-group, then engine), one line each. |
| Save a file to |
There are also 5 MCP prompts and 2 resource templates (cache://page/{url},
cache://search/{query_hash}). A twelfth tool, ask, appears only when an
answer backend is configured (see Delegating a lookup).
search and research take these filters:
Param | Values | Effect |
|
| Only results from the last N. Results with no date are kept, and |
|
| Restrict to these domains |
|
| Remove these |
| a group ( | Routes to the sources that natively index that kind of thing; the enum in the tool schema lists every value |
|
| Substring required in title/snippet |
|
| Substring forbidden |
|
| Accept a cached answer only if it is younger than this. Default 7 days; the tightest of this, the |
Output is Markdown by default, with provenance and a token estimate in the
header. format="json" returns structured data.
How a search runs
A search that names no engines asks a small keyless pool:
duckduckgo,bing,yahoojp(Yahoo! JAPAN, the keyless route to Google's index) andanysearch.googlenewsjoins when recency is asked for,so360andquarkwhen the query is written in Chinese,naverwhen it is written in Korean. Sources that only served a CAPTCHA or an empty shell (mojeek,google,startpage,searx,sogou) were removed rather than left to fail on every fresh process.category=routes to sources that index that kind of thing (papers, filings, datasets, packages, CVEs, …). Record sources such aspypi,nvdoropenmeteoalso join by themselves when the question is one they can answer directly.An engine that serves a CAPTCHA, a login wall or an off-topic page is benched, a reserve takes its seat, and the response says what happened. A proxy (
SEARCH_MCP_PROXY) is the fix for IP gating.Once you have signed in to
codex, it searches beside those engines on every such search and its results join the same ranking (the agent lane). The links never wait more than 6 s for it.With an answer backend configured (
SEARCH_MCP_AGENT_BACKEND=api,claude-codeorcodex),searchalso runs the answer agent beside the engines from the same result set and puts a short sourced answer above the links.SEARCH_MCP_AGENT_IN_SEARCH=falseturns that off;askstays available either way.Every result says how old it is and where its date came from, so the agent can tell a lead from a checked fact.
engines()prints the whole source tree.engines=["so360", "baidu"]runs exactly the engines named.
No engine that a search reaches by itself needs a key. The details, with measurements, are in docs/HOW_IT_WORKS.md; walls and proxies in docs/PROXY_AND_GATES.md; a tour of the tools with examples in docs/USAGE.md.
Install
The plugin in Quick start is the recommended path, because the server is pinned to the plugin's version and updates with it. The other routes run the same server:
Where | How |
Claude Desktop | open |
MCP Registry |
|
Claude Code, without the plugin |
|
Codex, without the plugin |
|
Hermes Agent (the plugin) |
|
Any other MCP client | the stdio command |
Docker |
|
A source checkout with Chromium, a smoke test and client registration in one step:
curl -LsSf https://raw.githubusercontent.com/sweetcornna/free-search-mcp/main/scripts/install.sh | bash -s -- --client claude-codeOver HTTP, uvx free-search-mcp --transport streamable-http --port 8000
serves http://127.0.0.1:8000/mcp. That endpoint has no authentication and
fetches any URL for whoever reaches it, so keep it on loopback or put an
authenticating proxy in front.
The JSON for Claude Desktop, Cursor, Cline, Continue and Zed, the installer's options and the plugin's token cost are in docs/INSTALL.md. Operating rules for agents are in docs/AGENT_USAGE.md.
Search on your own account: codex and antigravity
Two opt-in engines search on an account you sign in with instead of an API
key. Neither is in any pool or route, and nothing changes for anyone who never
signs in. Once you sign in to codex, it searches beside the keyless engines
on every automatic search (the agent lane); antigravity
runs only when a call or SEARCH_MCP_AGENT_ENGINES names it.
|
| |
Searches with | OpenAI's web search, the one Codex uses | Google Search, run by a Gemini model |
Account | a ChatGPT plan that includes Codex | a Google account with Antigravity |
Each search counts against | the plan's Codex usage | the account's Antigravity quota |
The provider allows it | yes | no, see the warning below |
Opens a sign-in page by itself | on first use, over stdio on a desktop | never |
Full guide, with 中文速览 |
Warning: Google's Antigravity terms forbid using its sign-in from third-party tools and name suspension of the Antigravity and Gemini CLI accounts as the consequence, and Google has suspended accounts for it. Sign in to
antigravityonly with an account you accept that risk for.
1. Sign in once
Run the sign-in on the machine the server runs on:
uvx --from free-search-mcp search-mcp-login codex # the ChatGPT page `codex login` shows
uvx --from free-search-mcp search-mcp-login antigravity # prints the warning, then Google's consent page
uvx --from free-search-mcp search-mcp-login status # account, plan, token expiry(uv run search-mcp-login … in a source checkout.) The settings page,
search-mcp-admin, has the same Sign in / 登录 buttons. Tokens are stored in
~/.config/search-mcp/oauth/ (0600) and refreshed automatically. The plugin,
uvx and the Desktop bundle all read that directory, so a running server picks
up a new sign-in without a restart.
codexcan skip this step: the first search that names it opens the sign-in page and finishes once you approve (stdio on a desktop only;SEARCH_MCP_CODEX_AUTO_SIGNIN=falseturns it off). A machine already signed in to the Codex CLI can reuse that sign-in read-only withsearch-mcp-login codex --use-codex-cli.antigravitysigns in with Antigravity's own OAuth client, which is not shipped in this package. The sign-in reads it from the Antigravity app installed on the machine (checked on macOS). Without Antigravity installed, setSEARCH_MCP_ANTIGRAVITY_CLIENT_IDandSEARCH_MCP_ANTIGRAVITY_CLIENT_SECRET; a machine that has signed in keeps both in~/.config/search-mcp/oauth/antigravity.json.
2. Name the engine
search("rust 2024 edition changes", engines=["codex"])
research("what changed in python 3.14 asyncio", engines=["antigravity"])
search("rust 2024 edition changes", engines=["codex", "duckduckgo", "bing"])In Claude Code or Codex, ask for it in words ("search this with the codex
engine"). Filters apply as usual, and results are cached like any other
search. Measured on 2026-09-26, a codex search took about 3 s and an
antigravity search 10 to 20 s.
3. Settings (all optional)
Var | Default | Meaning |
|
|
|
|
| seconds for one search |
|
| open the sign-in page on first use |
|
| the agent lane: |
|
| how long a search waits for the lane |
|
| how much longer than the keyless engines it waits, once they have results |
| read from the Antigravity install | for a machine without Antigravity; set both or neither |
| empty | the sign-in and the searches go through it |
The agent lane
A signed-in codex does not wait to be named. Every search that names no
engines asks it at the same moment as the keyless pool, and its results are
fused into the same ranking, so the agent search and the keyless search run in
parallel and one answer comes back. The keyless pool keeps its breaker,
reserves, routing and rescue, which naming engines=["codex", ...] would
switch off.
The search waits for the lane at most 6 s from when the engines were asked (
SEARCH_MCP_AGENT_LANE_WAIT_SECONDS), and at most 2 s past the keyless engines once they have results. A lane engine still searching then finishes in the background, and its results are merged into the cached result set, so the same search a little later returns them.The lane never queues for a rate-limit token, a spent usage window benches it at once, and its problems are reported under
agent_lane, never as a failed search.SEARCH_MCP_AGENT_ENGINES=offturns it off;codex,antigravityaddsantigravityto it.
On a server, over SSH, or in Docker
Sign in with
--no-browser(search-mcp-login codex --no-browser, orantigravity --no-browser), open the printed address in any browser, and approve. The browser then fails to load a127.0.0.1:1455orlocalhost:51121address; paste that whole address into the terminal.Over
streamable-http,codexnever opens a sign-in page by itself, since it would open on the server. Sign in there with the command above first.In Docker, point
SEARCH_MCP_CONFIG_DIRat a mounted volume so the tokens outlive the container, and run the sign-in inside it with--no-browser.antigravitythere needs the two client variables.
search-mcp-login logout codex or logout antigravity deletes the stored
tokens. To revoke Antigravity's access itself, remove it under "Third-party
apps and services" in the Google account.
When it does not work
The error says | Do this |
| no sign-in is stored on this machine; run the sign-in above |
| another sign-in ( |
the usage limit, quota or rate limit was reached | wait until the reset time the error gives; the keyless engines still work |
| install Antigravity, or set the two client variables |
HTTP 403 | set |
| sign in again |
Optional: your own API key
You do not need one, and agents should never ask for one. Six engines run only when a call names them and the operator has set their own key:
Engine | Provider | Key |
|
| |
|
| |
|
| |
|
| |
|
| |
|
|
Set a key as an environment variable or .env line, or on the local settings
page:
uv run search-mcp-admin # local settings / 本地设置 — http://127.0.0.1:8765(uvx --from free-search-mcp search-mcp-admin for a plugin or uvx install.)
The page is bilingual (中英双语), binds to 127.0.0.1 only, applies a saved
key without a restart, and never shows a stored value again. The walkthrough
per provider is docs/API_KEYS.md.
Delegating a lookup
Delegation keeps page text out of the caller's context and returns a short answer with dated sources. None of it is on by default:
You want | Use | It needs |
The agent to search and read by itself | the tools, as installed | nothing |
Your host's own subagents to do the lookup | the | a host with subagents |
The server to dispatch through Claude Code |
| the |
The server to dispatch through Codex |
| the |
The server to call a model endpoint you choose |
| an OpenAI-compatible or Anthropic-compatible URL |
Your own code to supply the model |
| nothing else |
The settings, the agent files and measured timings are in docs/DELEGATION.md.
Configuration
Nothing is required. Settings are SEARCH_MCP_* variables, read from the
environment first, then ./.env, then ~/.config/search-mcp/.env
(SEARCH_MCP_CONFIG_DIR moves that directory). The ones people change most:
Var | Default | Meaning |
| empty | outbound proxy ( |
| empty | proxy only these engines |
|
| the pool a search uses when it names no engines |
|
| locale for engines that take one |
|
| search and page cache |
| empty | register only these tools |
Every setting is listed in docs/CONFIGURATION.md and documented in .env.example.
Development
git clone https://github.com/sweetcornna/free-search-mcp.git && cd free-search-mcp
uv sync && uv run playwright install chromium
uv run pytest -q # offline
SEARCH_MCP_TEST_NETWORK=1 uv run pytest -v # live, hits the real webRunning claude inside the checkout picks up the repo's .mcp.json, which
starts the working tree's server. The architecture is in
docs/HOW_IT_WORKS.md, and cutting a
release in docs/RELEASING.md.
Credits
This project builds on:
mrkrsl/web-search-mcp: the httpx-then-Playwright fetch strategy and the 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), the SQLite FTS5 cache idea, and multi-formatread_documentnickclyde/duckduckgo-mcp-server: per-engine rate limiting and LLM-friendly content cleanup
License
MIT. See LICENSE.
Available Tools
11 toolscache_searchSearch local cache (FTS5)ARead-onlyIdempotent
Full-text search over pages already fetched into the local SQLite FTS5 index.
Best for:
- Recalling something the user/agent fetched earlier in the conversation
("what did that Wikipedia page say about X").
- Avoiding re-fetching content already in the local cache.
- Quick keyword grep across the corpus you've built up.
Not recommended for:
- Discovering new pages on the open web -> use `search` or `research`.
- When the cache is empty (fresh install) -> `search`/`research` first to
populate it.
Returns:
- markdown (default): a per-hit list of title, URL, and a `[bracket]`-
highlighted snippet around the matched terms.
- json: list of {url, title, snippet, author, date, sitename}. The last
three are "" when the cached row predates metadata capture.
Common mistakes:
- Treating this like web search: it ONLY hits pages already in the local
cache. If the user hasn't fetched anything, you'll get zero hits.
- Using natural-language phrases without quoting them; FTS5 splits on
whitespace as AND. For an exact phrase use `"like this"`.
Args:
query: FTS5 query. Bare terms = AND. Supports OR / NOT, prefix
(`term*`), and phrase (`"exact phrase"`).
limit: Max hits to return.
format: "markdown" or "json".
Inputs: query (required), limit, format.
compareCompare URLs side-by-sideARead-onlyIdempotent
Fetch 2-5 URLs concurrently and return per-URL excerpts so the LLM can compare them against a single question in one round trip.
Best for:
- Side-by-side product/feature/article comparisons.
- "Compare X to Y" or "How does A differ from B" queries.
- Triangulating a fact across multiple sources.
Not recommended for:
- >5 URLs -> use `fetch_batch`.
- 1 URL -> use `fetch`.
- Don't have URLs yet -> use `search` or `research` first.
Returns:
- markdown (default): a comparison brief with per-URL sections, each
containing title, sitename, published date, and a smart-truncated excerpt.
- json: {question, urls, excerpts:[{url, title, excerpt, ...}],
tokens_estimated}.
Common mistakes:
- Asking `compare` to actually answer the question: it returns material,
the LLM does the comparison.
- Passing >5 URLs and expecting them all to fit in context: use
`fetch_batch` for bulk reads.
Args:
question: The comparison question the LLM will answer using the
returned excerpts.
urls: 2-5 absolute http(s) URLs.
format: "markdown" (default) or "json".
Inputs: urls (required), question (required), format.
downloadDownload a file to diskAIdempotent
Save a file from a URL to a local, auto-expiring download directory.
Downloads are enabled by default and saved under
`SEARCH_MCP_CACHE_DIR/downloads`. Set `SEARCH_MCP_DOWNLOAD_ENABLED=false`
to disable them or `SEARCH_MCP_DOWNLOAD_DIR` to override the destination.
Best for:
- Keeping an actual file (installer, dataset, archive, image) rather than
its text.
- Handing a path to another tool that needs a real file on disk.
Not recommended for:
- Reading a document's contents -> use `read_doc`, which parses it without
touching the filesystem.
- Looking at a web page -> use `fetch`.
- Viewing an image -> use `fetch(inline=True)`.
Returns:
- markdown (default): where the file was saved, its size and type.
- json: {url, saved_path, media_type, bytes_size, sha256, expires_in_hours}.
An expires_in_hours value of 0 means TTL cleanup is disabled.
Retention: files older than SEARCH_MCP_DOWNLOAD_TTL_HOURS (default 24) are
deleted before the next download and at startup. A value of 0 disables TTL
cleanup. Otherwise, treat the path as short-lived and copy it elsewhere if
you need to keep it.
Args:
url: Absolute http(s) URL of the file to save.
format: "markdown" or "json".
Inputs: url (required), format.
enginesList available search enginesARead-onlyIdempotent
List the available sources, grouped by what they index.
Best for:
- Choosing a source deliberately: which one indexes filings, or preprints,
or Chinese-language pages.
- Checking a name before passing it to `engines=` on `search` / `research`.
Not recommended for:
- Calling on every search: the list is static, so read it once.
Returns (markdown): a `group -> sub-group -> engine` tree, one line of
description per engine. `group="paper"` restricts it to that group.
Returns (json): `{"engines": [...names...], "taxonomy": {...},
"descriptions": {...}}`.
Prefer `category=` over `engines=`. `category="paper"` WIDENS: it routes to
one specialist per sub-group. A dotted sub-group NARROWS: `"paper.biomed"`
queries only the biomedical indexes. Naming engines explicitly turns that
routing off entirely, so reach for it only to force a specific source.
Common mistakes:
- Passing one of these names as `query`: they belong in `engines=`.
- Passing a key-only engine with no key configured; it returns an
actionable error, not results.
Inputs: group, format.
extract_structuredExtract structured data from a URLARead-onlyIdempotent
Pull JSON-LD, OpenGraph, Twitter cards, and microdata from a web page.
Best for:
- Product pages (price, currency, availability, brand, rating).
- Article pages (author, publish date, image, headline).
- Recipe / event / video pages where rich metadata IS the answer.
- Cases where `fetch` returns prose but you need fields.
Not recommended for:
- Just reading a page -> use `fetch`.
- PDFs / DOCX -> use `read_doc`.
- Pages that don't publish schema.org metadata (most blogs): you'll get
empty lists; fall back to `fetch`.
Returns:
- json: {url, json_ld:[], microdata:[], opengraph:[], rdfa:[]}. Twitter
card meta tags are surfaced inside the `opengraph` list.
- markdown (default): a flattened key/value view with each block printed
as a JSON code block under its syntax heading.
Common mistakes:
- Calling on every URL "just in case": most sites have no structured
data, and `fetch` is what you actually want.
Args:
url: Absolute http(s) URL.
format: "markdown" (default) or "json".
Inputs: url (required), format.
fetchFetch a URL: page text, document, or resourceARead-onlyIdempotent
Fetch one URL: page text, or a description of a non-text resource.
Handles any http(s) resource, not just HTML:
- HTML pages -> reader-mode Markdown (nav/footer/scripts stripped).
- PDF/DOCX/XLSX/PPTX/EPUB/CSV/code/archives -> parsed text (same engine as
`read_doc`, which you should prefer when you need pagination).
- Images, video, audio, fonts, opaque binaries -> a description
(media type, byte size, dimensions, sha256), NOT the bytes.
Best for:
- You already have a URL (from `search`, the user, or your own knowledge)
and need the actual page text.
- Verifying a single claim by reading the source.
- Checking what a resource IS before deciding to spend tokens on it.
Not recommended for:
- Multiple URLs at once -> use `fetch_batch` (concurrent, one round-trip).
- "Search then read top N" -> use `research` (one call, not two).
- Long documents you need to page through -> use `read_doc` (start/length).
- You don't have a URL yet -> use `search` first.
Returns:
- markdown (default): a small header (URL, render method, token count)
plus the cleaned page body.
- json: {url, title, content, method, truncated, tokens_estimated,
author, published_date, sitename}, plus {media_type, bytes_size, sha256,
width, height} for non-text resources.
- With `inline=True` on an image: the image itself, viewable by a
vision-capable model.
Common mistakes:
- Passing a search query instead of a URL.
- Using `render="http"` on a JS-only SPA: it returns near-empty content;
use "auto" (default) or "browser".
- Setting `inline=True` on a large image out of habit. A 1MB image costs
well over a thousand tokens; fetch it plainly first and inline only if
the description says it's worth looking at.
- Forgetting that results are cached 7 days: use `force_refresh=True`
or `max_age_hours=0` for a fresh pull. The header says `cached N days ago`
when you are not looking at the live page; for deadlines, prices and
anything else that moves, that is the cue to refresh.
- Reading `no publication date found` as "recent". It means unknown.
Args:
url: Absolute http(s) URL.
render: "auto" (try HTTP, fall back to stealth Chromium), "http"
(fast, fails on JS), "browser" (slow, robust).
force_refresh: Bypass the page cache entirely.
max_age_hours: Treat cached pages older than this as a miss. 0 = same
as force_refresh. None = server default TTL (7 days).
inline: For images only. Returns the image itself instead of a
description, so a vision-capable model can see it. Ignored for
text resources.
format: "markdown" or "json".
Inputs: url (required), format, inline, render, +2 more.
fetch_batchFetch many URLs concurrentlyARead-onlyIdempotent
Fetch a list of URLs in parallel. Per-URL failures do not raise.
Best for:
- 2+ URLs you want to read in one round-trip.
- Reading the top N results of a previous `search` call.
Not recommended for:
- A single URL -> `fetch` (no list-wrapping overhead).
- "Search and then read" -> `research` collapses both into one tool call.
- PDFs/DOCX -> `read_doc` per file.
Returns:
- markdown (default): each page rendered as a Markdown section, separated
by horizontal rules; failed URLs become inline error notes.
- json: list[dict], one entry per URL, with `error` set on failures.
Common mistakes:
- Passing a single URL inside a 1-element list: use `fetch` directly.
- Assuming an exception means the whole batch failed; check each item's
`error` field instead.
Args:
urls: List of absolute http(s) URLs (max 20 per call).
render: Same as `fetch`.
format: "markdown" or "json".
Inputs: urls (required), format, render.
paper_graphWalk a paper's citation graphARead-onlyIdempotent
Follow the citations of ONE paper, and check whether it still stands.
`search` finds papers that MENTION your words. This follows the edges
instead: what a specific paper built on, and what has built on it since.
Best for:
- Checking a citation before repeating it: is the DOI real, and has the
paper been retracted or corrected?
- "What happened after this result": citing works come back ordered by how
much the field cited them, so a 2019 paper leads to the current state of
the art rather than to the most recent preprint about it.
- Building a reading list backwards from one good paper.
Not recommended for:
- Finding papers by topic -> `search(category="paper")`, or a sub-group
like `"paper.biomed"` / `"paper.cs"` / `"paper.preprint"`.
- Reading the paper itself -> `read_doc` on the returned URL.
Returns:
- markdown (default): the paper with its retraction/correction notices,
then "References" and "Cited by" sections.
- json: {paper, references, citations, notes}, where `paper.crossref`
carries `registered` and every post-publication `notices` entry.
Common mistakes:
- Passing a topic instead of a paper. A title resolves to its single best
match; a phrase that names no specific paper resolves to the wrong one.
- Reading an empty `citations` list as "uncited" when `notes` says the
lookup was truncated.
Args:
paper: DOI (`10.1145/1571941.1572114`, or a doi.org URL), an OpenAlex
ID (`W2148972377`), or the paper's exact title.
direction: "both", "references" (what it cites) or "citations" (what
cites it).
limit: Max neighbours per direction, 1-50.
format: "markdown" or "json".
Inputs: paper (required), limit, format, direction.
read_docRead a remote (or sandboxed local) documentARead-onlyIdempotent
Read an http(s) document (or a sandboxed local file) into Markdown.
Best for:
- Remote PDFs and DOCX from an http(s) URL (parsed locally, no remote API).
- Local PDF/DOCX/text/Markdown files, ONLY when local reads are enabled
(see Security below).
- Paginating through a long document via `start` / `length`.
Not recommended for:
- Arbitrary HTML web pages -> `fetch` does reader-mode cleanup that this
tool does not.
- Pages discovered through search -> `fetch` or `research`.
Security (local files are sandboxed and OFF by default):
- Local-file reads are DISABLED unless the server operator sets the
SEARCH_MCP_DOCUMENT_ROOT env var to a directory. With it unset, a local
path raises a "local file reads are disabled" error. Pass an http(s)
URL instead, or ask the operator to enable the sandbox.
- When enabled, `source` must resolve INSIDE that root; relative paths
resolve against the root (not the process CWD) and any `..` traversal
that escapes the root is rejected. `file://` URLs are always rejected.
- Remote http(s) sources are unaffected by this setting.
Returns:
- markdown (default): rendered document text with a small header.
- json: {content, title, format, total_chars, start, returned_chars,
truncated}. Use `total_chars` and `returned_chars` to drive pagination.
Common mistakes:
- Calling this on a normal article URL: you'll get raw HTML noise. Use
`fetch` instead.
- Forgetting to advance `start` when paginating: next call should pass
`start = previous_start + returned_chars`.
- Passing a negative `length` (raises an error) or a `start` past the end
(clamped to EOF: you'll get `returned_chars == 0`, `start == total_chars`,
and `truncated == False`, which is the signal you've paged off the end).
Args:
source: http(s) URL, or a local path UNDER SEARCH_MCP_DOCUMENT_ROOT when
local reads are enabled (disabled by default; see Security).
start: Character offset to begin reading from. Default 0. Clamped into
[0, total_chars]; a negative value is treated as 0.
length: Max characters to return; None = read to end (still capped by
the per-call max content size). Must be >= 0. A negative length
is rejected with a ValueError.
format: "markdown" or "json".
Inputs: source (required), start, format, length.
researchSearch and read in one callARead-only
One-shot research: search the web, fetch the top results, return both.
Best for:
- Open-ended questions that need finding sources AND reading them
("what's new with X", "summarize the controversy around Y").
- Replacing a `search` + N x `fetch` chain with one call.
- Producing a citable brief with [n]-style source references.
Not recommended for:
- You only need links -> `search` (cheaper, no fetching).
- You only need to read one URL you already have -> `fetch`.
- You want to query previously-fetched cached pages -> `cache_search`.
- Checking or expanding one paper's citations -> `paper_graph`.
Returns:
- markdown (default): a "Research brief" with a Sources index then the
full Markdown body of each fetched document, separated by horizontal
rules; includes a token estimate.
- json: {question, engines, sources:[{rank,title,url,snippet,...}],
documents:[...], tokens_estimated, errors}.
Common mistakes:
- Using `depth=8` for a quick lookup: that's 8 page fetches, and 2-3 is
almost always enough.
- Calling `research` for a known URL: that is what `fetch` is for.
- Forgetting that `fetch=False` returns sources only (much cheaper if
the LLM only needs to pick which one to read).
Args:
question: What you want to know, in natural language.
depth: How many top results to fetch (1-8). 3 is a good default.
engines: Override the engine set (see `engines()` for names). Prefer
`category=`. Naming engines turns category routing off.
fetch: If False, return source list without reading them.
freshness: "day"|"week"|"month"|"year". Restricts to recent results.
Best-effort; undated results are kept rather than dropped.
include_domains: Restrict to these domains (e.g. ["python.org"]).
exclude_domains: Drop results from these domains.
category: Which KIND of source to search; a bare group widens, a dotted
sub-group narrows. Same values as `search`; see `engines()`.
include_text: Substring required in title or snippet (case-insensitive).
exclude_text: Substring forbidden in title or snippet.
use_cache: Reuse cached search/page data within TTL.
max_age_hours: Treat cached search results AND cached page bodies older
than this as a read miss; fresh data is always written back. 0 =
force-refresh both the engine search and every fetched page body;
None = server default TTL (7 days). A non-zero value is honored for
both halves (it used to be ignored for anything but 0).
format: "markdown" or "json".
Inputs: question (required), depth, fetch, format, +9 more.
searchWeb search (multi-engine, no API key)ARead-only
Run a multi-engine web search and return a ranked, deduplicated link list.
Best for:
- Discovery queries ("what is X", "find me X", "who is X").
- Getting a list of URLs you can hand to `fetch` / `fetch_batch` next.
- Topics likely to be after your knowledge cutoff (use `freshness="week"`).
- Filtering to specific domains (`include_domains=["python.org"]`) or
a kind of source (`category="paper"` / `"finance"`, or a sub-group like
`category="paper.biomed"` / `"finance.filings"`; see `engines()` for the
full tree).
- Looking a fact up at its registry, with no category needed: a current
version or support end date ("latest fastapi version"), a CVE
("CVE-2024-3094": NVD record, OSV advisories, CISA exploited status),
a forecast ("上海明天天气"), an exchange rate ("100 usd to cny": ECB, and
the PBOC parity for 人民币), a coin price, a country indicator ("china
gdp", "japan population"), public holidays ("2026年放假安排"), the current
time in a city, a domain's expiry, a company's legal entity, an iOS
app's version, Wikidata facts ("Shanghai population"). The source that
fits the words joins the search and its record arrives as the first
result with the publisher's date in `dated …`, so it can be cited
without a fetch. Name the thing plainly. `category=` ("software",
"security", "weather", "finance.fx", "stats", "calendar", "reference",
"docs", "gov") asks the same sources explicitly.
Not recommended for:
- You already know the URL -> use `fetch` instead.
- You want both links AND their full text in one call -> use `research`.
- You want to query pages already in the local cache -> use `cache_search`.
- Reading PDFs/DOCX from a known URL -> use `read_doc`.
- Following one paper's references or citations -> use `paper_graph`.
Returns:
- markdown (default): numbered list of `n. title`, `<url>`, snippet. About 40%
fewer tokens than json. When an answer agent is configured
(`SEARCH_MCP_AGENT_BACKEND`), a short sourced `## Answer` block sits above
the list; it starts beside the engines and answers from the same result
set.
- json: dict with `results` (list of {title,url,snippet,engines,score}),
`engines`, `cached`, optional `errors` map, optional `hint` string, and
`agent` ({answer, backend, model, sources, elapsed_seconds} or
{error}) when an answer agent is configured.
`engines` is what was actually ASKED for this answer; each result's own
`engines` is what found it. A default engine that failed recently is not
asked and appears under `benched_engines` instead; a rescue pass that
substituted a source appears as `rescued_via`. When the operator has
signed in to an agent engine (`codex`), it searches beside the keyless
engines and `agent_lane` reports it ({engines, answered, pending, errors,
rate_limited, benched, left_out}; `left_out` lists an answer dropped as
off-topic or unconfirmed);
one still searching when the links return is merged into the cached
result set, so the same call repeated later carries its results too.
Common mistakes:
- Answering from snippets. A snippet is an engine's summary of a page as it
looked when crawled: it drops qualifiers and is often years old. Use
`search` to find the URL, then read the page (`fetch`, `research`) for
any date, amount, rule, deadline or number you will state.
- Treating the `## Answer` block as verified. It is one small model's
reading of the few pages below; read the source page yourself before
acting on a deadline, price or rule.
- Treating a result as recent because `freshness=` was set. The filter keeps
undated results; the header says how many could be dated (`dated: 3/10`)
and each result is marked `dated …`, `… (from snippet text)` or `undated`.
- Passing a URL as `query`: that is `fetch`'s job.
- Cranking `max_results` up hoping for better recall; engines cap around
10-20 each, anything beyond is duplicate noise (and 50 is the ceiling).
- Naming engines by default. The default pool is already the set that works
keylessly over plain HTTP, it benches an engine that starts failing and
seats a reserve when it gets thin, and naming engines switches all of
that, and `category=` routing, off. Name engines when you want a
specific index: `engines=["so360","baidu"]` for Chinese-language sites,
`engines=["brave"]` (needs the browser) for a second opinion.
- Using `category="news"` for breaking news without also setting
`freshness="day"`: the index lags by days.
Args:
query: Natural-language query (the same string a human would type).
engines: Subset of `engines()`. None (recommended) = the health-aware
default pool: duckduckgo, bing, yahoojp (Google's index) and
anysearch, plus googlenews when `freshness` is "day"/"week",
so360 and quark for a Chinese query and naver for a Korean one.
max_results: Merged result count after dedup, clamped to 1-50. It is
also the PER-ENGINE budget, so it multiplies across the fan-out;
5-20 is the useful range and anything past that is duplicate noise
bought with real latency. Omit it for the configured default.
use_cache: Reuse the last result for this exact (query, engines,
max_results, AND all active filters: freshness, include/exclude
domains, category, include/exclude text) within the cache TTL.
Changing any filter is a different cache entry. False forces a
re-fetch.
max_age_hours: Treat cached results older than this as a read miss; a
fresh result is ALWAYS written back to the cache regardless of this
value, so caching is never disabled. Use 0 to force-refresh while
keeping cache writes; None = use server default TTL (7 days).
freshness: "day"|"week"|"month"|"year". Restricts to recent results.
Best-effort: applied as an engine time-window param AND a client-side
date check, but most HTML-engine results carry no parseable date, so
undated results are kept rather than dropped (unknown != old). Treat
it as a strong hint, not a hard filter; googlenews dates are exact.
include_domains: List of domains to restrict to (e.g. ["python.org"]).
exclude_domains: List of domains to exclude.
category: Which KIND of source to search. The enum lists every value.
A bare group widens: "paper" adds one specialist per sub-group to the
default web pool. A dotted sub-group narrows to just the sources that
index it ("paper.biomed" => the biomedical indexes only). It also
RERANKS: engines that natively index the category count double in
the fusion, so the filing outranks the commentary about it, and a
record looked up by the query (a PyPI release, a CVE, an ECB rate,
a forecast) counts five times, so it leads the list. Call
`engines()` for the group -> sub-group -> engine tree with a line on
each source. Two behaviours worth knowing: "image"/"dataset" REPLACE
the web pool rather than augment it (a web engine cannot return an
image file), and "news"/"paper"/"forum"/"github"/"blog" also filter
general-web hits by hostname, so a strict category can thin those
engines out. The specialists it routes to are exempt.
include_text: Substring required in title or snippet (case-insensitive).
exclude_text: Substring forbidden in title or snippet.
format: "markdown" (default) or "json".
Inputs: query (required), format, engines, category, +8 more.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 11 tools
Each tool has a sharply bounded purpose, and the descriptions actively preempt confusion with explicit 'Not recommended for' cross-references (fetch vs fetch_batch vs compare vs research, read_doc vs fetch on documents, search vs cache_search). The one genuinely thin boundary — compare vs fetch_batch for multi-URL reads — is spelled out by URL-count and output-shape rules.
All names are lowercase snake_case with a consistent style, and most follow verb_noun or verb_object (fetch_batch, cache_search, read_doc, extract_structured). A few are bare verbs or nouns (search, fetch, compare, download, engines, research) and paper_graph is noun_noun, but the set still reads predictably.
Eleven tools is squarely in the sweet spot for a search-and-retrieval server. Every tool maps to a distinct workflow primitive (discover, read, batch, compare, extract, cache, cite, save) with no filler.
The surface covers the full lifecycle: discovery (search/research/engines), retrieval (fetch/fetch_batch/read_doc with pagination), inspection (extract_structured), local corpus reuse (cache_search), citation tracing (paper_graph), and file persistence (download). Only very minor cache-management gaps exist (no listing or eviction of cached pages), which agents can work around.
Maintenance
Related MCP Connectors
Web search API and MCP server for developers building AI agents.
Free web search for AI agents. No API key required. Hosted MCP in active development.
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.
Multi-engine search for AI agents. Trust scoring, local corpus, MCP-native. Self-hostable, BYOK.
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
- AlicenseNot gradedqualityBmaintenanceA zero-config web search and fetch MCP server for LLM agents, featuring multi-backend metasearch, persistent rolling cache, and structured error envelopes for retry-friendly interactions.MIT
- AlicenseNot gradedqualityFmaintenanceMCP server enabling local-first web search, fetch, extract, and caching with citeable excerpts, no API key required. Supports research workflows for agents and apps.72 npm1MIT
- AlicenseAqualityBmaintenanceAn MCP server providing web search, image search, and page scraping tools to LLMs without requiring API keys.12323MIT