Universal Web Retrieval MCP
Universal Web Retrieval MCP provides resilient web search and page-content retrieval through a configurable provider chain with automatic failover.
Search the web with
web_searchusing failover or concurrent fan-out/RRF result merging.Fetch a single URL or batch up to 5 URLs as Markdown/clean text with
web_fetch.Override providers per call or configure search/fetch chains, disabled providers, and search mode via environment variables.
Inspect available providers, capabilities, configured/enabled status, and active chain positions with
list_providers.Automatically use keyed or keyless provider auth, skip unconfigured keyed-only providers, and return diagnostic metadata like
provider,mode,latency_ms, andattempts.Benefit from bounded timeouts, provider failover, error classification, URL deduplication, and rank fusion in fan-out mode.
Provides web search through the Brave Search API as a configurable provider in the server's search chain, using a Brave API key and supporting automatic failover with other search providers.
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., "@Universal Web Retrieval MCPsearch for the latest AI news"
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.
Universal Web Retrieval MCP
A universal MCP server providing resilient web search and content retrieval through a configurable provider chain — AnySearch, Tavily, DDGS, Tencent Yuanbao Search, Bocha, DeepSeek, Kimi, Zhipu BigModel, Brave Search and Jina Reader — with automatic failover.
Quick Start
No API key is required. With no keys configured the server uses the keyless mode of AnySearch/Tavily and falls back to keyless DDGS.
uvx (zero config, recommended):
{"mcpServers": {"universal-web-retrieval": {"command": "uvx", "args": ["uwr"]}}}npx (same server, no Python toolchain needed — the launcher fetches a runner):
{"mcpServers": {"universal-web-retrieval": {"command": "npx", "args": ["-y", "@withgardener/uwr"]}}}pip:
pip install uwr
uwr # stdio MCP serverAPI keys are optional: add TAVILY_API_KEY, ANYSEARCH_API_KEY (or any of
the others) to the env block when you have them. See Configuration.
Related MCP server: Search Toolkit
Overview
Give any MCP-capable agent reliable web access through a single server. Universal Web Retrieval routes each request through an ordered provider chain, picks keyed or keyless authentication automatically based on which API keys are present, and moves to the next provider on rate limits, outages, or upstream errors.
The active chains are selected in this order:
providersargument passed for the call.Environment configured chain (
UWR_SEARCH_CHAINorUWR_FETCH_CHAIN).Built-in default (
anysearch,tavily,ddgs).
providers=None selects the configured chain. An explicitly empty list, providers=[], is invalid input; it never means "use defaults". Jina Reader works keyless and is deliberately absent from both defaults. New providers are opt-in, preserving the default behavior from 0.1.x.
Agent (any MCP client)
│
├── web_search ──▶ chain from UWR_SEARCH_CHAIN (default: AnySearch → Tavily → DDGS)
└── web_fetch ──▶ chain from UWR_FETCH_CHAIN (default: AnySearch → Tavily → DDGS.extract)Features
Three standard tools —
web_search,web_fetch, andlist_providers, clean JSON in/out.Configurable chains —
UWR_SEARCH_CHAIN/UWR_FETCH_CHAINset the order;UWR_DISABLED_PROVIDERSswitches providers off without editing the chains.Per-call override —
web_search(..., providers=["bocha"])or["yuanbao","tavily"]runs just that order for one call.Automatic provider failover — one provider failing never fails the tool.
Opt-in fan-out —
mode="fanout"queries up to N providers concurrently and rank-fuses the answers (RRF) instead of taking the first one. See Search modes.Auto keyed/keyless — key configured → official keyed API; key absent → official keyless access (where the provider offers one). Keyed-only providers (Yuanbao, Bocha, DeepSeek, Kimi, Zhipu, Brave) without a key are skipped, never attempted, so a missing key cannot 401 the chain.
Strict official APIs — every provider is called through its official documented endpoint and auth mechanism. No scraping hacks, no undocumented endpoints.
Error classification — rate limits / outages fall over; invalid credentials fail loudly (ERROR log) and move to the next provider; malformed input never triggers fallback.
Bounded latency — per-provider timeouts are capped to the remaining chain budget; the router does not start a provider call once less than 100 ms remain.
Observable — every response carries
provider,mode,latency_msand anattemptstrail.Zero agent coupling — standard MCP stdio; works with Hermes, Claude Code, OpenCode, or any MCP client.
Architecture
src/universal_web_retrieval/
├── server.py # MCP tool surface (web_search / web_fetch / list_providers)
├── config.py # env-driven settings, call-time key + chain reads
├── errors.py # ProviderError classification, result models, Provider ABC
├── fanout.py # URL dedupe + Reciprocal Rank Fusion merge (fan-out mode)
└── providers/
├── registry.py # name -> provider instance; capability lookup
├── router.py # chain resolution (env / override / default) + failover + fan-out
├── anysearch.py # AnySearch (keyed Bearer / keyless no-auth-header)
├── tavily.py # Tavily (keyed Bearer / keyless X-Tavily-Access-Mode)
├── ddgs.py # DDGS (official package; text + extract)
├── yuanbao.py # Tencent Yuanbao Search (MCP over Streamable HTTP, Bearer)
├── bocha.py # Bocha Web Search API (REST, Bearer)
├── deepseek.py # DeepSeek Search + Fetch API (REST, Bearer)
├── kimi.py # Kimi/Moonshot search + search_pro + fetch (REST, Bearer)
├── zhipu.py # Zhipu BigModel web_search (REST, Bearer)
├── brave.py # Brave Search API (REST, X-Subscription-Token)
└── jina.py # Jina Reader r.jina.ai (keyless or Bearer, fetch only)Installation
pip install uwr # from PyPI
uvx uwr # or run without installing
npx -y @withgardener/uwr # or through the npm launcher
uwr --version # uwr 0.3.0
uwr # run the stdio MCP serverddgs is a runtime dependency (the fixed final fallback of the default chains), not an optional extra. Dependencies are pinned to compatible ranges (mcp>=2,<3, httpx>=0.27,<1, ddgs>=9.16,<10) so a major-version breaking change never ships to a running server via a routine pip update.
From source (development)
git clone https://github.com/withgardener/universal-web-retrieval-mcp.git
cd universal-web-retrieval-mcp
pip install -e .
uwr # or: PYTHONPATH=src python -m universal_web_retrievalConfiguration
Chains
Env var | Default | Effect |
|
| Ordered search chain, e.g. |
|
| Ordered fetch chain (search-only providers are dropped) |
| (empty) | Names switched off everywhere, e.g. |
|
|
|
|
| Max providers queried concurrently in fan-out mode (≥ 1, clamped to the chain length) |
|
| Seconds to wait for stragglers after the first fan-out answer; |
Resolution rules:
A per-call
providersargument wins (it only affects that call).Otherwise
UWR_SEARCH_CHAIN/UWR_FETCH_CHAIN.Otherwise the built-in defaults above — 0.2.0 is behaviour-compatible with 0.1.x out of the box.
Unknown names, providers lacking the capability, and disabled providers are dropped with one warning per distinct chain (not once per request). A chain that resolves to nothing falls back to the defaults. An explicitly empty/invalid per-call override is an input error instead — providers=["bocha"] never silently becomes the whole default chain.
Keys and tuning
Env var | Required | Effect |
| no | AnySearch keyed mode; absent → official keyless |
| no | Tavily keyed mode; absent → official keyless ( |
| for | Tencent Yuanbao Search service key |
| for | Bocha Web Search API key |
| for | DeepSeek API key (Search + Fetch API) |
| for | Kimi / Moonshot platform key |
| for | Zhipu BigModel (智谱) key |
| for | Brave Search API subscription token |
| no | Jina Reader key — optional (keyless 20 RPM, with free key 500 RPM) |
| no |
|
| no |
|
| no | Overall chain deadline in seconds (default 45) |
| no |
|
| no | Cap for |
| no | Max fetched-content chars (default 15000) |
| no | AnySearch connect/read timeouts (5s / 20s) |
| no | Tavily connect/read timeouts (5s / 20s) |
| no | DDGS overall timeout (15s) |
| no | Yuanbao timeouts (5s / 20s) |
| no | Bocha timeouts (5s / 20s) |
| no | DeepSeek timeouts (5s / 20s) |
| no | Kimi timeouts (5s / 20s) |
| no | Zhipu timeouts (5s / 20s) |
| no | Brave timeouts (5s / 20s) |
| no | Jina timeouts (5s / 30s — Reader drives a browser) |
| no | Override the Yuanbao MCP endpoint |
| no |
|
| no | Default |
| no |
|
| no | Default |
| no | Default |
| no | Default |
Every key is read at call time, so a restart of the MCP server process is enough for new values to take effect (for Hermes: the mcp_servers env is frozen at gateway start — restart the gateway or kill the child process).
Tools
web_search(query, limit=5, providers=None, mode=None)
{
"success": true,
"provider": "anysearch",
"mode": "keyless",
"latency_ms": 1555,
"attempts": [{"provider": "anysearch", "outcome": "ok"}],
"results": [{"title": "...", "url": "...", "snippet": "..."}]
}When a provider exposes publication date / site (Yuanbao, Bocha, DeepSeek, Kimi, Zhipu, Brave), the result rows also carry published, site and (where scored) score. provider / mode / latency_ms / attempts are diagnostic metadata — consumers only need results.
mode selects the search mode for this call ("failover" or "fanout", see Search modes); omit it to use UWR_SEARCH_MODE. In fan-out mode provider is "fanout", mode is the shared auth mode of the answering providers (or "mixed"), attempts entries for successful providers also carry results and latency_ms, and every result row carries sources (provider names in chain order):
{
"success": true, "provider": "fanout", "mode": "keyed", "latency_ms": 812,
"attempts": [
{"provider": "bocha", "outcome": "ok", "results": 5, "latency_ms": 640},
{"provider": "tavily", "outcome": "ok", "results": 5, "latency_ms": 790},
{"provider": "ddgs", "outcome": "skipped:grace"}
],
"results": [{"title": "...", "url": "...", "snippet": "...", "sources": ["bocha", "tavily"]}]
}attempts is the per-provider outcome trail in chain order:
Outcome | Meaning |
| This provider produced the answer |
| Attempted and failed; the chain moved on |
| Keyed-only provider without a key — never called |
| The chain deadline passed before this provider could start (failover) or answer (fan-out) |
| (fan-out only) Still running when the grace window after the first answer closed; its result is discarded |
| (fan-out only) Eligible chain member beyond the configured N-provider fan-out cutoff |
Success schema (search): {"success": true, "provider": ..., "mode": ..., "latency_ms": ..., "attempts": [...], "results": [...]}. Provider error schema: {"success": false, "error": "...", "attempts": [...]}. Invalid-input schema: {"success": false, "error": "invalid input: ..."} (no attempts). Fetch returns one object per URL with url, content, content_type, title, provider, mode, latency_ms, and attempts; failed URLs carry error and attempts instead of success metadata.
Sample failure of a mixed chain (UWR_SEARCH_CHAIN=yuanbao,bocha,ddgs with no Yuanbao/Bocha keys):
{
"success": false,
"error": "all providers failed: yuanbao: skipped (not_configured) | bocha: skipped (not_configured) | ddgs(keyless): HTTP 429 rate limited: ...",
"attempts": [
{"provider": "yuanbao", "outcome": "skipped:not_configured"},
{"provider": "bocha", "outcome": "skipped:not_configured"},
{"provider": "ddgs", "outcome": "error:HTTP 429 rate limited: ..."}
]
}Per-call override:
{"query": "腾讯 财报", "providers": ["bocha"]} // Bocha only
{"query": "腾讯 财报", "providers": ["yuanbao", "tavily"]} // try Yuanbao firstAn override that contains no usable provider (including providers=[]) is an input error. providers=None means use the configured chain; an empty explicit list never falls back to the defaults.
web_fetch(url | urls, providers=None)
Accepts a single url string or a urls array (max 5) and an optional providers override. Returns a JSON array:
[{
"url": "https://example.com",
"content": "# Example Domain\n\n...",
"content_type": "text_markdown",
"title": "Example Domain",
"provider": "anysearch",
"mode": "keyless",
"latency_ms": 191,
"attempts": [{"provider": "anysearch", "outcome": "ok"}]
}]Failed URLs carry an error field instead of content. Content is Markdown/clean text — never raw HTML. JS-rendered, login-walled, or CAPTCHA-protected pages are not guaranteed (plain HTTP retrieval only; no browser automation).
list_providers()
Returns every provider this build knows, with capability, configuration status, enabled flag and its 1-based position in each active chain, plus the active search_mode and the fan-out settings (fanout.n, fanout.grace_s). Key values are never returned.
{
"chains": {"search": ["anysearch", "tavily", "ddgs"], "fetch": ["anysearch", "tavily", "ddgs"]},
"defaults": {"search": ["anysearch", "tavily", "ddgs"], "fetch": ["anysearch", "tavily", "ddgs"]},
"search_mode": "failover",
"fanout": {"n": 3, "grace_s": 5.0},
"disabled": [],
"providers": [
{"name": "bocha", "kind": "search", "capabilities": ["search"], "keyed_only": true,
"configured": false, "auth_mode": null, "enabled": true, "position": {"search": null}},
{"name": "anysearch", "kind": "search", "capabilities": ["fetch", "search"], "keyed_only": false,
"configured": true, "auth_mode": "keyed", "enabled": true,
"position": {"search": 1, "fetch": 1}}
]
}Network scope & SSRF note
web_fetch retrieves any URL the host environment can reach — this deliberately includes private-network and localhost targets, which is a legitimate capability for a local agent tool. Redirects may land on private or link-local destinations. When deploying against untrusted agent input, apply network-level isolation (container/netns/firewall) as appropriate for your threat model. A future WEB_RETRIEVAL_BLOCK_PRIVATE=1 opt-in may add in-process filtering; it is intentionally not implemented yet.
Batch behavior: urls is capped at 5 per call (larger batches are rejected with an explicit error, not silently truncated), duplicates are de-duplicated preserving first-seen order, and the whole batch shares one deadline — URLs whose turn arrives after the deadline return a timeout error without any network attempt. Error text containing URLs has credential-looking query parameters (token=, api_key=, ...) redacted.
Routing & fallback
Capability | Default chain | Optional members, on request only |
| AnySearch → Tavily → DDGS | Yuanbao, Bocha, DeepSeek, Kimi, Zhipu, Brave |
| AnySearch → Tavily → DDGS.extract | DeepSeek, Kimi, Jina (search-only providers are dropped from fetch chains) |
A link is skipped when: rate-limited (429), timed out, upstream 5xx, or any provider error. Invalid credentials fail loudly (ERROR log AUTH FAILURE on <provider>) and the chain moves to the next provider — a wrong key is never silently retried as keyless on the same provider. Malformed input (empty query, bad URL scheme) returns an immediate input error with no fallback.
Example chains (not defaults). Defaults stay minimal so existing deployments are unchanged. If you hold the keys, this is a sensible China-friendly ordering — deepseek/kimi give independent indexes and correct published dates, and ddgs stays last as the keyless floor:
UWR_SEARCH_CHAIN: anysearch,tavily,deepseek,kimi,yuanbao,ddgs
UWR_FETCH_CHAIN: anysearch,tavily,deepseek,kimi,ddgsEvery one of those links is either free or priced per call (see the table below), and each one falls through on error, so a longer chain costs nothing when earlier links succeed. Two rules worth keeping:
Keep
ddgslast — keyless, no quota, the floor of the chain.Never put
jinain a default chain — it works without a key, so a stale/keyless deployment would start sending every fetch through it. Add it explicitly when you want its Markdown quality, e.g.UWR_FETCH_CHAIN: anysearch,tavily,jina,ddgs.
Search modes
web_search has two modes. The default is unchanged from every earlier release.
Mode | Behaviour | Cost per search |
| Walks the chain in order; the first provider that returns OK wins, even with no hits | one provider call |
| Queries the first N eligible providers concurrently, waits | N provider calls |
The trade-off: fan-out spends N× provider quota per search, and its latency is
bounded by the grace window (at most UWR_FANOUT_GRACE seconds more than the
fastest provider) and by the shared chain deadline (WEB_RETRIEVAL_TIMEOUT) —
never by the slowest provider. In exchange it buys agreement: a page several
providers return outranks a page only one of them found, which is what you want
when a wrong first hit is expensive.
Latency and quota
Measured on the default chain (anysearch,tavily,ddgs) with N=3 and the default
5 s grace:
Scenario | Wall time |
| ~2 s |
| ~7 s |
| ~2.5 s |
In the slow case the first two providers answered at about 2.1 s and ddgs was
still running when the grace window closed: the response came back at about 7 s
with ddgs recorded as skipped:grace. All three calls were made either way, so
every fan-out spends N× provider quota — including the ones whose answers are
thrown away. An abandoned provider keeps running in its daemon thread and may
still complete (and be billed) in the background; the grace window decides what
you wait for, not what you pay for.
Selecting a mode:
UWR_SEARCH_MODE=fanout(orfailover) sets the default for every call. An unknown value logs one warning and falls back tofailover.web_search(..., mode="fanout")overrides the default for one call. An unknown per-call value is an input error — it never silently switches modes.In fan-out mode
providersstill defines the candidate order; its first N eligible entries are queried. Keyed-only providers without a key are recorded asskipped:not_configuredand do not consume a slot.UWR_SEARCH_FANOUT(default3, minimum1, clamped to the chain length) sets N.
Merging (fanout.py):
URLs are normalised for dedupe into a scheme-less key,
//host[:port]/path?query, sohttp://andhttps://for one page collapse — as does a default port (:80for http,:443for https). The host is IDNA-normalised with any leadingwww.stripped (non-default ports and IPv6 literals preserved), the fragment and tracking parameters (utm_*,gclid,fbclid,mc_cid,mc_eid,ref,ref_src,spm) dropped, remaining parameters sorted, and a trailing/removed (except for the root path). The output keeps the best-ranked occurrence's original URL, not the key.Reciprocal Rank Fusion:
score = Σ 1/(60 + rank)over every provider that returned the URL, with a 1-based rank inside that provider's list. Ties break on number of sources, then best single rank, then chain order.Per row: title and URL from the best-ranked occurrence, the longest non-empty snippet, and
published/sitefrom the first non-empty value in chain order. Every row gainssources— the provider names, in chain order.
Fan-out responses set provider: "fanout" and mode to the shared auth mode of
the answering providers, or "mixed" when they differ. attempts carries one
entry per queried provider: ok (plus results and latency_ms),
error:<msg>, skipped:not_configured, skipped:fanout (beyond the N-provider
cutoff), skipped:grace (a late successful answer is discarded; a late error stays visible) or skipped:deadline. Success
requires at least one provider to answer OK, not at least one hit: if every
answering provider returns an empty list, the response is a success with
results: [] and ok / results: 0 entries in attempts — the same thing
failover returns for that query. Only when no provider answers OK at all is it
the same all providers failed: ... error with the trail attached, and one
provider's fallback_eligible=False error does not abort the others.
{"query": "rust async runtime", "mode": "fanout",
"providers": ["bocha", "tavily", "ddgs"]}web_fetch is always failover: there is nothing to merge for a single URL.
Concurrency: providers are synchronous HTTP clients, so fan-out starts one daemon
thread per selected provider — plain threading.Thread plus a queue.Queue of
(provider, outcome) records, no new dependency and no private interpreter
internals. Each worker gets its own timeout budget and shares no mutable per-call
state. Yuanbao is the only provider holding per-instance state (MCP session id and
JSON-RPC counter); both are guarded by an RLock, and a concurrent handshake race
costs one extra session, not correctness. A straggler that outlives the grace
window keeps running in its daemon thread; the collector never joins it, it cannot
delay interpreter exit, and the result it eventually posts is dropped.
Provider behavior
The built-in chain remains anysearch,tavily,ddgs for search and fetch; all newly added providers are opt-in unless explicitly listed in UWR_SEARCH_CHAIN, UWR_FETCH_CHAIN, or a per-call providers override. Jina is keyless but deliberately absent from defaults.
See docs/providers.md for each provider's official endpoint, auth mechanism, keyless behavior, pricing notes and last-verified date.
Provider | Capabilities | Auth | Key env | Price / free tier | Notes |
AnySearch | search + fetch | keyed Bearer / keyless (no auth header) |
| plan-based | default first link |
Tavily | search + fetch | keyed Bearer / keyless header |
| 1,000 free credits/month | default second link |
DDGS | search + fetch | none | — | free, no quota | default last link; JS/login pages not guaranteed |
Yuanbao (元宝搜索) | search | keyed only |
| 14–26 CNY / 1k calls by tier | Tencent MCP endpoint, |
Bocha (博查) | search | keyed only |
| 0.036 CNY/call; 1,000 free trial calls (3 months) |
|
DeepSeek | search + fetch | keyed only |
| $0.75 / 1k successful calls (same for search and fetch) |
|
Kimi (Moonshot) | search + fetch | keyed only |
| $0.002 basic search, $0.003 pro search, $0.002 fetch; billed only on HTTP 200 with results |
|
Zhipu BigModel (智谱) | search | keyed only |
|
|
|
Brave Search | search | keyed only |
| $5.00 / 1k requests, $5 free credits every month (≈1k requests) |
|
Jina Reader | fetch | keyless (20 RPM) or keyed Bearer (500 RPM free key) |
| free tier: 20 RPM keyless, 500 RPM with a free key |
|
Official sources for every row are listed in docs/providers.md. Prices are catalogue list prices as published on 2026-10-20 and may change; the provider's own pricing page is authoritative.
Examples
All provider keys below are placeholders or environment-variable references; never put live credentials in client configuration files.
Generic MCP client (stdio)
{
"mcpServers": {
"universal-web-retrieval": {
"command": "uvx",
"args": ["uwr"],
"env": {
"ANYSEARCH_API_KEY": "${ANYSEARCH_API_KEY}",
"TAVILY_API_KEY": "${TAVILY_API_KEY}",
"UWR_SEARCH_CHAIN": "anysearch,tavily,bocha,ddgs"
}
}
}
}Swap "command": "uvx", "args": ["uwr"] for "command": "npx", "args": ["-y", "@withgardener/uwr"]
where npm is the only package manager available, or for
"command": "python", "args": ["-m", "universal_web_retrieval"] when running the
from-source checkout with PYTHONPATH set.
Hermes Agent
In ~/.hermes/config.yaml:
mcp_servers:
universal-web-retrieval:
command: /root/.local/bin/uvx
args: ["uwr"]
transport: stdio
enabled: true
env:
ANYSEARCH_API_KEY: ${ANYSEARCH_API_KEY}
TAVILY_API_KEY: ${TAVILY_API_KEY}
# optional, only if the corresponding key is provided:
# YUANBAO_API_KEY: ${YUANBAO_API_KEY}
# BOCHA_API_KEY: ${BOCHA_API_KEY}
# DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY}
# MOONSHOT_API_KEY: ${MOONSHOT_API_KEY}
# ZHIPU_API_KEY: ${ZHIPU_API_KEY}
# BRAVE_API_KEY: ${BRAVE_API_KEY}
# JINA_API_KEY: ${JINA_API_KEY}
# UWR_SEARCH_CHAIN: anysearch,tavily,deepseek,kimi,yuanbao,ddgs
# UWR_FETCH_CHAIN: anysearch,tavily,deepseek,kimi,ddgsClaude Code
claude mcp add universal-web-retrieval \
--env TAVILY_API_KEY="${TAVILY_API_KEY}" \
-- uvx uwrCursor
Add a project .cursor/mcp.json or user MCP configuration with environment variables supplied by the launching process:
{
"mcpServers": {
"universal-web-retrieval": {
"command": "uvx",
"args": ["uwr"],
"env": {
"ANYSEARCH_API_KEY": "${ANYSEARCH_API_KEY}",
"TAVILY_API_KEY": "${TAVILY_API_KEY}"
}
}
}
}For Cursor releases using its ${env:NAME} interpolation syntax, use "${env:ANYSEARCH_API_KEY}" and "${env:TAVILY_API_KEY}". Verify environment substitution on your Cursor version; do not replace these references with literal credentials.
Development
pip install pytest
pytest tests/unit -v # chain/override/attempts/provider unit tests (no network)
# Live provider smoke tests (hit real APIs; opt-in):
RUN_LIVE_TESTS=1 pytest tests/integration -vUnit tests clear provider keys and UWR_* variables per test, so the suite is hermetic — it passes with or without keys exported in your shell. New providers are covered with mocked HTTP only (no keys needed, no live calls).
License
MIT
Available Tools
3 toolslist_providersA
List every provider this server knows: capabilities (search/fetch), whether it is usable right now (configured / key present), whether it is enabled, and its 1-based position in each active chain. Also reports the active search mode (failover/fanout) and the fan-out fan width/grace. Never returns key values. Use it to decide what to pass in web_search/web_fetch providers.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries the burden and it does so well: it discloses that key values are never returned, that results include live usability state and chain ordering, plus active search mode and fan-out width/grace. It does not mention permissions, side effects, or cost/latency, but nothing contradicts a read-only listing.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences, front-loaded with the resource and scope, followed by the concrete reason to call it. Every clause adds information (fields returned, security guarantee, routing purpose) with no padding.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need not be documented, yet the description still previews the key result fields, which is a bonus rather than a redundancy. For a zero-parameter listing tool, an agent has everything needed to call it and interpret the result.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool takes zero parameters, so the baseline is 4. The description's reference to the `providers` argument belongs to sibling tools, not this one, but that context is still useful for connecting the call to downstream usage.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('List every provider this server knows') and enumerates exactly what is reported: capabilities, usability, enabled state, and 1-based chain positions. This is clearly distinct from the action siblings web_search and web_fetch, which it explicitly references.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives a concrete usage trigger: 'Use it to decide what to pass in web_search/web_fetch `providers`.' That is a clear routing context. It does not state any when-not conditions or alternative discovery paths, so it falls just short of the top band.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
web_fetchA
Fetch one URL's page content as Markdown with automatic provider failover (default: AnySearch -> Tavily -> DDGS.extract; operators can reorder the chain with UWR_FETCH_CHAIN). Auth is automatic (keyed when a key is configured, else official keyless). Accepts a single url string, or urls array (max 5) for batch. providers overrides the chain order for this call. Returns JSON array of {url, content, title, content_type, provider, mode, latency_ms, attempts} — failed URLs carry an error field instead. Note: JS-rendered, login-walled or CAPTCHA-protected pages are not guaranteed.
| Name | Required | Description | Default |
|---|---|---|---|
| url | No | ||
| urls | No | ||
| providers | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and does so: it names the default provider chain and its env-var override, explains that auth is automatic (keyed or keyless), documents the per-URL error field, and warns that JS-rendered, login-walled or CAPTCHA pages may fail. That is exactly the operational context an agent needs before committing to a fetch.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the core purpose, then parameters, then return shape, then caveats — a sensible order. It is a dense single block, and listing all eight return fields is slightly redundant given an output schema exists, but no sentence is wasted.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a 3-parameter fetch tool with no annotations, the description covers selection, batching limits, provider override, auth behavior, failure semantics, and known page-type limitations. Nothing an agent needs to call this correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 0%, so the description must compensate, and it does: it defines `url` as a single string, `urls` as a batch capped at 5, and `providers` as a per-call chain override. It leaves one ambiguity — which parameter wins if both `url` and `urls` are supplied, and that neither is marked required.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Fetch one URL's page content as Markdown') plus the failover mechanism, which immediately distinguishes it from web_search. An agent knows this retrieves page bodies rather than search results without opening the schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Conveys usage shape (single `url` vs `urls` batch of max 5, `providers` overriding the chain for one call) but never states when to prefer this over web_search or list_providers. The distinction is inferable from the names, not from explicit guidance or exclusions.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
web_searchA
Web search through a configurable provider chain with automatic failover. Two modes: mode="failover" (default) walks the chain one provider at a time and returns the first success (including an empty answer); mode="fanout" queries up to UWR_SEARCH_FANOUT providers concurrently, then merges and rank-fuses the results (even an all-empty set of OK answers succeeds with results: []; provider="fanout", every row carries sources; costs N× provider quota per search). Default chain: AnySearch -> Tavily -> DDGS; operators can reorder/extend it with UWR_SEARCH_CHAIN (e.g. anysearch,tavily,yuanbao,bocha,deepseek,kimi,ddgs) and pick the default mode with UWR_SEARCH_MODE. Auth is automatic: a configured API key enables keyed mode, otherwise the provider's official keyless access is used. Returns JSON: {success, provider, mode, latency_ms, attempts, results: [{title, url, snippet, published?, site?, sources?}]}. providers overrides the chain order for this one call (e.g. ["bocha"] or ["yuanbao","tavily"]); in fanout mode its first N eligible entries are queried.
| Name | Required | Description | Default |
|---|---|---|---|
| mode | No | ||
| limit | No | ||
| query | Yes | ||
| providers | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and does so well: it discloses auth behavior (configured key enables keyed mode, otherwise keyless), configurable chain/mode via env vars, the quota cost of fanout, and that an all-empty set of OK answers still counts as success with `results: []`. These are exactly the non-obvious traits an agent needs.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The purpose is front-loaded in the first sentence, followed by mode semantics and then configuration details. It is dense but nearly every clause carries operational meaning; the redundant explicit return-shape listing is the main piece that overlaps with the existing output schema.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a 4-parameter tool with no annotations, it covers modes, auth, chain configuration, cost, and failure semantics, and an output schema exists so return values need no explanation. The only material gap is the undocumented `limit` parameter.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 0% (bare titles only), so the description must compensate, and it does for most parameters: `mode` values 'failover'/'fanout' are defined, `providers` is explained with examples and the 'first N eligible' rule in fanout. `query` is self-evident, but `limit` (default 5) is never mentioned, leaving one parameter undefined.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb+resource ('Web search') plus the distinctive mechanism (configurable provider chain with automatic failover) and the two execution modes. It is unambiguous about what the tool does, but never mentions the siblings web_fetch or list_providers, so the agent gets no explicit differentiation from them.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives clear conditions for the main decision: failover walks the chain and returns the first success, fanout queries up to UWR_SEARCH_FANOUT providers concurrently and costs N× provider quota — enough to pick a mode. It also explains when to pass `providers`. It lacks guidance on when to use this versus web_fetch or list_providers.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
3 tool updates
v0.3.0- Added
list_providers - Changed
web_fetch1 field changed- added
Input schema / properties / providersAdded value: +{ + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Providers" +}
- Changed
web_search2 fields changed- added
Input schema / properties / modeAdded value: +{ + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Mode" +} - added
Input schema / properties / providersAdded value: +{ + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Providers" +}
2 tool updates
v0.1.0- First observed
web_fetch - First observed
web_search
TDQS
Scored across 3 tools
Each tool has a distinct purpose: web_search performs search, web_fetch retrieves page content, and list_providers reports provider/chain metadata. There is no overlap that would cause an agent to reasonably choose the wrong tool.
The names are in readable snake_case with verb_noun structure for web_search and web_fetch, while list_providers slightly deviates by omitting the web_ prefix. The inconsistency is minor and does not impair predictability.
Three tools are well-scoped for a web retrieval server: one for search, one for fetching, and one for provider discovery. Each earns its place without redundant surface area.
The core lifecycle of discovering providers, searching, and fetching content is covered. Minor gaps remain around dedicated provider health checks, richer extraction options, or batch search, but agents can work around them.
Maintenance
Related MCP Connectors
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.
Free web search for AI agents. No API key required. Hosted MCP in active development.
Scrape, crawl and search the web for AI agents via MCP.
Your agent needs the open web — searched by more than one engine, and read as clean markdown rather than raw HTML. **What you can ask for** • "Search this question with two providers and tell me where they disagree." • "Scrape these 40 URLs into markdown, in one batch." • "Crawl this documentation site and give me every page." • "Do deep research on this topic and cite the sources." • "Find the academic papers behind this claim." **How to use it** Point any MCP client at https://mcp.aisa.one/search/mcp and sign in with OAuth — there is no key to create or paste. 30 tools across several independent providers: Tavily and Exa search, answers, contents and agent runs; Firecrawl scrape, batch scrape, crawl, map and search; Perplexity Sonar, Sonar Pro, reasoning and deep research; Oxylabs AI search and LLM jobs; OpenAI and Anthropic web search; and scholarly search. **Why this rather than the source** Several independent indexes behind one account, because one engine's blind spot is not visible from inside it. **It is also a door to the rest** The same login reaches 26 sources and 580+ operations. Find the page here, then ask the same agent who links to it or how much traffic it gets — without adding a second server. **What it costs** Finding and inspecting an operation is free. Running one is billed per call at API prices, with no seat and no monthly minimum, and every call takes max_price_usd so an agent cannot overspend by accident. **Where else it reaches** https://mcp.aisa.one/seo-serp/mcp for the Google results page itself, https://mcp.aisa.one/seo-serp-other-engines/mcp for Bing, Baidu and Naver.
Related MCP Servers
- AlicenseAqualityBmaintenanceEnables AI agents to perform multi-engine web search, fetch web pages, and extract clean Markdown content via MCP, with no API keys required.368 PyPI8MIT
- AlicenseNot gradedqualityBmaintenanceEnables AI agents to perform web search, extraction, and research across multiple provider backends with persistent multi-key rotation and auditable routing. Supports MCP, CLI, and Agent Skill entry points.MIT
- AlicenseNot gradedqualityBmaintenanceEnables MCP clients to run unified web searches across multiple providers such as Exa, Serper, Tavily, Firecrawl, and Jina, fusing results with reciprocal rank fusion, and to extract readable content from public web pages, PDFs, and video pages as Markdown. Provider chains are configurable, failures are isolated per provider, and responses report attempted, succeeded, failed, and skipped channels with source tracing and honest warnings.MIT
- AlicenseAqualityBmaintenanceEnables MCP-compatible clients to search the web and fetch HTTP/HTTPS pages as Markdown, returning result metadata such as titles, URLs, dates, relevance, and summaries.224 npm1MIT