Providers
BetterWebSearch MCP uses a provider abstraction layer. Each provider implements the same SearchProvider interface, so the aggregation layer treats them uniformly. Providers are enabled or disabled based on environment keys.
Provider overview#
| Provider | Key needed? | Env var | Status |
|---|---|---|---|
| DuckDuckGo | No | — | Always on (keyless fallback) |
| Brave | Yes | BRAVE_API_KEY | Primary — better ranking & recency |
| Tavily | Yes | TAVILY_API_KEY | Optional — when present |
| SerpApi | Yes | SERPAPI_KEY | Stub — not yet implemented |
Keyless-first design#
DuckDuckGo is always enabled and requires no API key. This means:
npx -y better-web-search-mcpworks immediately — no configuration needed- The test suite (189+ tests) runs fully keyless
- Other providers are opt-in — add
BRAVE_API_KEYfor richer results
SearchProvider interface#
Every provider implements this interface:
interface SearchProvider {
/** Stable provider identifier (e.g. "brave", "tavily", "duckduckgo"). */
name: string;
/**
* Run a search and return normalized results.
* Implementations MUST NOT throw on provider errors (missing key, HTTP
* 401/429, timeout). They return an empty array and log a warning instead,
* so one failing provider never breaks the aggregate search.
*/
search(query: string, opts: SearchOptions): Promise<SearchResult[]>;
}Each provider normalizes its native API response into a common SearchResult shape:
interface SearchResult {
title: string;
url: string;
snippet: string;
published?: string; // ISO date string
score?: number; // 0-1 relevance score
source: string; // provider name
}Aggregation#
Providers are aggregated via Promise.allSettled — one failing provider never aborts the others. Rejected providers are logged as warnings and skipped; fulfilled results are flattened into a single array.
async function aggregateSearch(query: string, opts: SearchOptions): Promise<SearchResult[]> {
const providers = enabledProviders();
const settled = await Promise.allSettled(
providers.map((provider) => provider.search(query, opts)),
);
// ... flatten fulfilled results
}Ranking pipeline#
After aggregation, results go through:
- Deduplication —
normalizeUrlstrips tracking params (utm, gclid, fbclid), trailing slashes, and hashes - Domain scoring — curated tiers assign authority scores
- Re-ranking —
0.5 × text_overlap + 0.2 × domain_score + 0.2 × recency + 0.1 × provider_score
Query expansion#
web_research / deep_search expands the input question into 4–6 parallel search variants before querying providers. For example:
- Input:
unlimited mobile internet Germany - Variants include:
unbegrenztes Datenvolumen Deutschland,unlimited data plan Germany 2026,best unlimited mobile Germany, etc.
All variants are searched in parallel across all enabled providers.
Provider-specific notes#
Brave#
- Provides freshness filtering (
pd/pw/pm/pytime ranges) - Supports extra snippets for deeper content
- Best ranking and recency when
BRAVE_API_KEYis set
Tavily#
- Optional secondary provider
- Enabled when
TAVILY_API_KEY(orBETTER_WEB_SEARCH_TAVILY_API_KEY) is set
DuckDuckGo#
- Always on, zero configuration
- Provides the baseline search capability
- Falls back gracefully when other providers are unavailable
SerpApi#
- Stub implementation — included in the provider list but returns no results
- Reserved for future development