search
Retrieve web results from multiple search engines with automatic fallback between Google, Tavily, DuckDuckGo, and Brave.
Instructions
Performs a web search using multiple providers (google, tavily, duckduckgo, brave).
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | The search query. | |
| provider | No | Optional: Specify a provider directly (google, tavily, duckduckgo, brave). If omitted, uses priority fallback. | |
| num_results | No | Number of results desired (default: 10). |
Implementation Reference
- src/index.ts:15-43 (registration)The tool name constant 'search' and its full schema definition including name, description, and inputSchema with parameters (query, provider, num_results).
const TOOL_NAME = 'search'; const TOOL_DEFINITION = { name: TOOL_NAME, description: 'Performs a web search using multiple providers (google, tavily, duckduckgo, brave).', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'The search query.', }, provider: { type: 'string', enum: ['google', 'tavily', 'duckduckgo', 'brave'], description: 'Optional: Specify a provider directly (google, tavily, duckduckgo, brave). If omitted, uses priority fallback.', }, num_results: { type: 'integer', description: `Number of results desired (default: ${DEFAULT_NUM_RESULTS}).`, default: DEFAULT_NUM_RESULTS, minimum: 1, maximum: 20, }, }, required: ['query'], additionalProperties: false, }, }; - src/index.ts:99-269 (handler)The main handler for the 'search' tool. Handles validation, provider selection (priority/random/requested), fallback logic, API key checks, calling search functions with timeout, result standardization, and formatting the MCP response.
this.server.setRequestHandler(CallToolRequestSchema, async (request: any) => { if (request.params.name !== TOOL_NAME) { throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); } const args = request.params.arguments; // Validate input if (!args.query || typeof args.query !== 'string' || args.query.trim() === '') { throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid required argument: query (string)'); } const query = args.query; const requestedProvider = args.provider; const numResults = args.num_results || DEFAULT_NUM_RESULTS; if (requestedProvider && !searchFunctions[requestedProvider]) { throw new McpError(ErrorCode.InvalidParams, `Invalid provider specified: ${requestedProvider}. Valid options are: google, tavily`); } if (requestedProvider === 'tavily') { if (!API_KEYS.tavily_key) { // Throw as InvalidParams because it's a configuration issue tied to the specific request throw new McpError(ErrorCode.InvalidParams, `Tavily API key is not configured for this server`); } } if (requestedProvider === 'google') { if (!API_KEYS.google_key || !API_KEYS.google_cx) { // Throw as InvalidParams because it's a configuration issue tied to the specific request throw new McpError(ErrorCode.InvalidParams, `Google API key or CX are not configured for this server`); } } // --- Start of logic to track fallback and order --- let providersAttemptedOrder: string[]; if (requestedProvider) { providersAttemptedOrder = [requestedProvider]; } else if (SEARCH_STRATEGY === 'random') { // Pick a random provider from the enabled list const enabled = PROVIDER_LIST.filter(p => searchFunctions[p]); if (enabled.length === 0) { throw new McpError(ErrorCode.InternalError, 'No enabled providers available for random selection.'); } const randomProvider = enabled[Math.floor(Math.random() * enabled.length)]; providersAttemptedOrder = [randomProvider]; } else { // Default: priority order (try all in order) providersAttemptedOrder = PROVIDER_LIST.filter(p => searchFunctions[p]); } // Fallback is considered triggered if no specific provider was requested, // meaning the server attempted providers based on the configured list. const fallbackTriggered = !requestedProvider; // --- End of logic to track fallback and order --- // Provider execution logic let finalResults: StandardizedSearchResult[] = []; let errorMessages: string[] = []; let successfulProvider: string | null = null; const timeoutMs = 30000; // 15 second timeout per provider for (const provider of providersAttemptedOrder) { // Use the list captured for tracking try { const searchFn = searchFunctions[provider]; const standardizeFn = standardizeFunctions[provider]; // --- Move API key check here, inside the loop before attempting the provider --- // This ensures we only throw missing key errors for providers we actually try // and prevents the loop from stopping if the *first* provider in the priority list // has a missing key but later ones might be configured. switch (provider) { case 'google': if (!API_KEYS.google_key || !API_KEYS.google_cx) { // Throw a standard Error here, which will be caught by the inner try/catch throw new Error('Google API key or CX missing'); } break; case 'tavily': if (!API_KEYS.tavily_key) { // Throw a standard Error here throw new Error('Tavily API key missing'); } break; case 'duckduckgo': // DuckDuckGo does not require an API key break; case 'brave': if (!API_KEYS.brave_key) { throw new Error('Brave Search API key missing'); } break; } // --- End of moved API key check --- // Prepare provider-specific arguments let providerArgs: any[]; switch (provider) { case 'google': providerArgs = [API_KEYS.google_key, API_KEYS.google_cx, numResults]; break; case 'tavily': providerArgs = [API_KEYS.tavily_key, numResults]; break; case 'duckduckgo': providerArgs = [numResults]; break; case 'brave': providerArgs = [API_KEYS.brave_key, numResults]; break; default: // Should not happen due to earlier checks, but good practice throw new McpError(ErrorCode.InternalError, `Internal error: Unknown provider ${provider}`); } // Execute search with timeout const rawResults = await Promise.race([ searchFn(query, ...providerArgs), new Promise((_, reject) => setTimeout(() => reject(new Error(`${provider} search timed out after ${timeoutMs}ms`)), timeoutMs)), ]); // Standardize results, passing the provider name // Add type assertion for rawResults from Promise.race const standardized = standardizeFn(rawResults as any[], provider); if (standardized.length > 0) { finalResults = standardized; successfulProvider = provider; break; // Exit loop on first success } else { console.error(`${provider} returned 0 results.`); // Don't add to errorMessages here, it's not an error, just no results } } catch (error: any) { // Catch errors thrown by providers (missing keys, API errors, timeouts) const message = `${provider}: ${error.message}`; console.error(`Error during search with ${provider}:`, error.message); errorMessages.push(message); } } // Check if any provider succeeded if (successfulProvider && finalResults.length > 0) { // Format results according to MCP spec (array of content items) const content = finalResults.map(item => ({ type: 'text', // Use 'text' type for the search results // Combine the details into a single text string for the 'text' content item text: `Title: ${item.title}\nLink: ${item.link}\nSnippet: ${item.snippet}\nSource: ${item.provider}`, })); // --- Construct the 'data' object with fallback information --- const responseData = { fallbackTriggered: fallbackTriggered, providersAttemptedOrder: providersAttemptedOrder, successfulProvider: successfulProvider, // Include the successful provider for clarity errorsDuringAttempt: errorMessages.length > 0 ? errorMessages : undefined // Optionally include errors if any occurred before success }; // --- End of 'data' object construction --- // Return MCP success response including content AND data return { content, data: responseData }; } else { // If no provider succeeded, throw an MCP error const combinedErrors = errorMessages.join('; '); console.error(`Search failed for query "${query}". Errors: ${combinedErrors}`); // You could also include the attempted providers list in the error data if needed // const errorData = { providersAttemptedOrder }; throw new McpError( ErrorCode.InternalError, // Or a more specific code if applicable `Search failed. No provider succeeded for query "${query}". Errors: ${combinedErrors || 'No results found or provider configuration issue.'}`, // errorData // Optional: pass errorData here ); } }); - src/providers/tavily.ts:11-54 (helper)Tavily search provider implementation. Calls the Tavily API with the query and API key, returns simplified search results.
export async function searchTavily(query: string, apiKey: string, numResults: number): Promise<SimplifiedSearchResult[]> { if (!apiKey) { // This check is also in the server handler, but good to have here too throw new Error('Tavily API key is missing'); } const baseUrl = 'https://api.tavily.com/search'; try { const response = await axios.post(baseUrl, { query: query, max_results: numResults, // Use max_results parameter include_answer: "basic", // Keep if you want the answer field (separate from results array) // include_images: false, // Add options as needed // include_deeplinks: true, }, { headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` } }); // Check if response data or results array is valid if (!response.data || !Array.isArray(response.data.results)) { console.error('Tavily API returned unexpected response format:', response.data); const errorDetails = response.data ? JSON.stringify(response.data).substring(0, 200) + '...' : 'No response data'; throw new Error(`Tavily API returned unexpected response format: ${errorDetails}`); } // Map the raw Tavily results to the simplified structure // Filtering for valid results (e.g., missing title/link/content) is better done in the standardize step // This mapping just assumes the basic structure exists and provides defaults return response.data.results.map((item: any) => ({ title: item.title || '', link: item.url || '', content: item.content || '', })).filter((item: SimplifiedSearchResult) => item.title && item.link && item.content); // Optional: Filter out results with no title, link, or content } catch (error: any) { console.error('Tavily Search API Error:', error.message); throw error; } } - src/providers/brave.ts:4-38 (helper)Brave Search provider implementation. Calls the Brave Search API with the query and API key, extracts web results.
export async function searchBrave(query: string, apiKey: string, numResults: number): Promise<any[]> { if (!apiKey) { throw new Error('Brave Search API key is missing'); } const baseUrl = 'https://api.search.brave.com/res/v1/web/search'; // Supported params: q, safesearch, spellcheck, country, search_lang, freshness, sources, extra_snippets, ai_summarization, goggles, page, use_autopaging const params: Record<string, any> = { q: query, count: numResults // Add more supported params here if needed }; try { const response = await axios.get(baseUrl, { params, headers: { 'X-Subscription-Token': apiKey, 'Accept': 'application/json', 'Accept-Encoding': 'gzip', }, }); const data: any = response.data; // Brave API: Only extract text (web) results, ignore videos and mixed if (data && data.web && Array.isArray(data.web.results)) { return data.web.results; } // Optionally handle locations if needed, else just return [] return []; } catch (error: any) { console.error('Brave Search API Error:', error.message); throw new Error(`Brave Search API Error: ${error.message}`); } } - src/utils/standardize.ts:17-90 (helper)StandardizedSearchResult interface and four standardize functions (Google, Tavily, DuckDuckGo, Brave) that transform raw provider results into a uniform format with title, link, snippet, and provider fields.
export interface StandardizedSearchResult { title: string; link: string; snippet: string; // Represents the description/content snippet provider: string; } export function standardizeGoogleResults(rawResults: RawGoogleResult[], providerName: string): StandardizedSearchResult[] { // Filter out results missing essential fields like snippet, title, or link const filteredResults = rawResults.filter(item => item.title && item.link && typeof item.snippet === 'string' && item.snippet.length > 0 ); return filteredResults.map((item: RawGoogleResult) => ({ title: item.title, link: item.link, snippet: item.snippet, provider: providerName, })); } export function standardizeTavilyResults(rawResults: RawTavilyResult[], providerName: string): StandardizedSearchResult[] { // Filter out results missing essential fields // Note: Tavily provider already maps raw results to { title, link, content } const filteredResults = rawResults.filter(item => item.title && item.link && typeof item.content === 'string' && item.content.length > 0 ); return filteredResults.map((item: RawTavilyResult) => ({ title: item.title, link: item.link, snippet: item.content, // Use 'content' as the snippet provider: providerName, })); } interface RawDuckDuckGoResult { title: string; link: string; snippet: string; } export function standardizeDuckDuckGoResults(rawResults: RawDuckDuckGoResult[], providerName: string): StandardizedSearchResult[] { const filteredResults = rawResults.filter(item => item.title && item.link && typeof item.snippet === 'string' && item.snippet.length > 0 ); return filteredResults.map((item: RawDuckDuckGoResult) => ({ title: item.title, link: item.link, snippet: item.snippet, provider: providerName, })); } interface RawBraveResult { title: string; url: string; description: string; } export function standardizeBraveResults(rawResults: RawBraveResult[], providerName: string): StandardizedSearchResult[] { const filteredResults = rawResults.filter(item => item.title && item.url && typeof item.description === 'string' && item.description.length > 0 ); return filteredResults.map((item: RawBraveResult) => ({ title: item.title, link: item.url, snippet: item.description, provider: providerName, })); }