web-search
Search the web using DuckDuckGo to find relevant information, web pages, and summaries for any query. Get results with titles, URLs, and detailed content to access real-time web data.
Instructions
Perform a web search using DuckDuckGo and receive detailed results including titles, URLs, and summaries.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | Enter your search query to find the most relevant web pages. | |
| numResults | No | Specify how many results to display (default: 3, maximum: 20). | |
| mode | No | Choose 'short' for basic results (no Description) or 'detailed' for full results (includes Description). | short |
Implementation Reference
- src/tools/searchTool.js:40-79 (handler)The handler function that processes input parameters, calls the underlying searchDuckDuckGo function, formats the search results into a markdown-like text structure, and returns the response in MCP format.export async function searchToolHandler(params) { const { query, numResults = 3, mode = 'short' } = params; console.log(`Searching for: ${query} (${numResults} results, mode: ${mode})`); const results = await searchDuckDuckGo(query, numResults, mode); console.log(`Found ${results.length} results`); // Format results as readable text, similar to other search tools const formattedResults = results.map((result, index) => { let formatted = `${index + 1}. **${result.title}**\n`; formatted += `URL: ${result.url}\n`; if (result.displayUrl) { formatted += `Display URL: ${result.displayUrl}\n`; } if (result.snippet) { formatted += `Snippet: ${result.snippet}\n`; } if (mode === 'detailed' && result.description) { formatted += `Content: ${result.description}\n`; } if (result.favicon) { formatted += `Favicon: ${result.favicon}\n`; } return formatted; }).join('\n'); return { content: [ { type: 'text', text: formattedResults || 'No results found.' } ] }; }
- src/tools/searchTool.js:6-33 (schema)Tool metadata and input schema definition, specifying parameters for query, number of results, and result detail mode.export const searchToolDefinition = { name: 'web-search', title: 'Web Search', description: 'Perform a web search using DuckDuckGo and receive detailed results including titles, URLs, and summaries.', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Enter your search query to find the most relevant web pages.' }, numResults: { type: 'integer', description: 'Specify how many results to display (default: 3, maximum: 20).', default: 3, minimum: 1, maximum: 20 }, mode: { type: 'string', description: "Choose 'short' for basic results (no Description) or 'detailed' for full results (includes Description).", enum: ['short', 'detailed'], default: 'short' } }, required: ['query'] } };
- src/index.ts:14-18 (registration)Adds the web-search tool definition to the list of available tools served via ListToolsRequestHandler.const availableTools = [ searchToolDefinition, iaskToolDefinition, monicaToolDefinition ];
- src/index.ts:49-52 (registration)Registers the searchToolHandler for execution when 'web-search' tool is called via CallToolRequestHandler.switch (name) { case 'web-search': return await searchToolHandler(args);
- src/utils/search.js:132-316 (helper)Underlying utility function that performs the actual DuckDuckGo web search: fetches HTML, parses results, extracts clean URLs, generates favicons, optionally scrapes content via Jina.ai, with caching and robust error handling.async function searchDuckDuckGo(query, numResults = 10, mode = 'short') { try { // Input validation if (!query || typeof query !== 'string') { throw new Error('Invalid query: query must be a non-empty string'); } if (!Number.isInteger(numResults) || numResults < 1 || numResults > 20) { throw new Error('Invalid numResults: must be an integer between 1 and 20'); } if (!['short', 'detailed'].includes(mode)) { throw new Error('Invalid mode: must be "short" or "detailed"'); } // Clear old cache entries clearOldCache(); // Check cache first const cacheKey = getCacheKey(query); const cachedResults = resultsCache.get(cacheKey); if (cachedResults && Date.now() - cachedResults.timestamp < CACHE_DURATION) { console.log(`Cache hit for query: "${query}"`); return cachedResults.results.slice(0, numResults); } // Get a random user agent const userAgent = getRandomUserAgent(); console.log(`Searching DuckDuckGo for: "${query}" (${numResults} results, mode: ${mode})`); // Fetch results with timeout const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT); try { const response = await axios.get( `https://duckduckgo.com/html/?q=${encodeURIComponent(query)}`, { signal: controller.signal, headers: { 'User-Agent': userAgent }, httpsAgent: httpsAgent, timeout: REQUEST_TIMEOUT } ); clearTimeout(timeoutId); if (response.status !== 200) { throw new Error(`HTTP ${response.status}: Failed to fetch search results`); } const html = response.data; // Parse results using cheerio const $ = cheerio.load(html); const results = []; const jinaFetchPromises = []; $('.result').each((i, result) => { const $result = $(result); const titleEl = $result.find('.result__title a'); const linkEl = $result.find('.result__url'); const snippetEl = $result.find('.result__snippet'); const title = titleEl.text()?.trim(); const rawLink = titleEl.attr('href'); const description = snippetEl.text()?.trim(); const displayUrl = linkEl.text()?.trim(); const directLink = extractDirectUrl(rawLink || ''); const favicon = getFaviconUrl(directLink); const jinaUrl = getJinaAiUrl(directLink); if (title && directLink) { if (mode === 'detailed') { jinaFetchPromises.push( axios.get(jinaUrl, { headers: { 'User-Agent': getRandomUserAgent() }, httpsAgent: httpsAgent, timeout: 8000 }) .then(jinaRes => { let jinaContent = ''; if (jinaRes.status === 200 && typeof jinaRes.data === 'string') { const $jina = cheerio.load(jinaRes.data); jinaContent = $jina('body').text(); } return { title, url: directLink, snippet: description || '', favicon: favicon, displayUrl: displayUrl || '', description: jinaContent }; }) .catch(() => { // Return fallback without content return { title, url: directLink, snippet: description || '', favicon: favicon, displayUrl: displayUrl || '', description: '' }; }) ); } else { // short mode: omit description jinaFetchPromises.push( Promise.resolve({ title, url: directLink, snippet: description || '', favicon: favicon, displayUrl: displayUrl || '' }) ); } } }); // Wait for all Jina AI fetches to complete with timeout const jinaResults = await Promise.race([ Promise.all(jinaFetchPromises), new Promise((_, reject) => setTimeout(() => reject(new Error('Content fetch timeout')), 15000) ) ]); results.push(...jinaResults); // Get limited results const limitedResults = results.slice(0, numResults); // Cache the results resultsCache.set(cacheKey, { results: limitedResults, timestamp: Date.now() }); // If cache is too big, remove oldest entries if (resultsCache.size > MAX_CACHE_PAGES) { const oldestKey = Array.from(resultsCache.keys())[0]; resultsCache.delete(oldestKey); } console.log(`Found ${limitedResults.length} results for query: "${query}"`); return limitedResults; } catch (fetchError) { clearTimeout(timeoutId); if (fetchError.name === 'AbortError') { throw new Error('Search request timeout: took longer than 10 seconds'); } if (fetchError.code === 'ENOTFOUND') { throw new Error('Network error: unable to resolve host'); } if (fetchError.code === 'ECONNREFUSED') { throw new Error('Network error: connection refused'); } throw fetchError; } } catch (error) { console.error('Error searching DuckDuckGo:', error.message); // Enhanced error reporting if (error.message.includes('Invalid')) { throw error; // Re-throw validation errors as-is } throw new Error(`Search failed for "${query}": ${error.message}`); } }