UNPKG

mcp-search-tools

Version:

MCP server and client for web search and page viewing tools - DuckDuckGo search and web scraping

150 lines 5.53 kB
import { Website } from '@spider-rs/spider-rs'; // Cheerio core import * as cheerio from 'cheerio'; import { z } from 'zod'; import { withRetry } from '../utils/retry.js'; export const webSearchSchema = z.object({ query: z.string().describe('Search query to execute'), maxResults: z .number() .optional() .default(10) .describe('Maximum number of results to return'), region: z .string() .optional() .default('wt-wt') .describe('Region for search results'), time: z .string() .optional() .describe('Time filter for search results (d, w, m, y)'), maxRetries: z .number() .optional() .default(3) .describe('Maximum number of retry attempts for failed requests'), retryDelay: z .number() .optional() .default(1000) .describe('Base delay in milliseconds between retry attempts'), }); export async function webSearchTool(input) { try { const results = await withRetry(() => performWebSearch(input), input.maxRetries, input.retryDelay); return { content: [ { type: 'text', text: JSON.stringify({ query: input.query, results: results.map((r) => ({ title: r.title, url: r.url, snippet: r.snippet, source: r.source, })), }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error performing web search after ${input.maxRetries} attempts: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } } async function performWebSearch(input) { const maxResults = input.maxResults || 10; const query = input.query; async function fetchSearch(url) { const w = new Website(url) .withChromeIntercept(true, true) .withBudget({ '*': 1 }) .build(); w.withHeaders({ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'Accept-Language': 'en-US,en;q=0.9', Referer: 'https://duckduckgo.com/', }); await w.scrape(); return w.getPages()[0] ?? null; } const primaryUrl = `https://duckduckgo.com/html/?q=${encodeURIComponent(query)}`; let page = await fetchSearch(primaryUrl); // Fallback to lite version if needed if (!page?.content || (page.statusCode && page.statusCode >= 400)) { const liteUrl = `https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`; page = await fetchSearch(liteUrl); } if (!page) { throw new Error(`No results retrieved for query "${query}"`); } // Helper to resolve relative URLs and extract DDG redirect links const extractLinks = ($, baseUrl, selector) => { const extractLink = (_, el) => { const $el = $(el); const hrefAttr = $el.attr('href'); if (!hrefAttr) return null; try { const url = new URL(hrefAttr, baseUrl); if (url.hostname.endsWith('duckduckgo.com') && url.pathname === '/l/') { const targetUrl = url.searchParams.get('uddg'); if (targetUrl) { return { href: targetUrl, text: $el.text().trim() }; } } return { href: url.toString(), text: $el.text().trim() }; } catch { return null; } }; return $(selector) .map(extractLink) .get() .filter(isNotNull); }; // Load once, then try different selectors const $ = cheerio.load(page.content || ''); let links = extractLinks($, page.url, 'a.result__a'); // Lite selectors fallback if (!links.length) { links = extractLinks($, page.url, 'td.result-link > a, a.result-link'); } links = links.slice(0, maxResults); // Last resort – retry lite interface directly if not already done if (!links.length) { const liteUrl = `https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`; const litePage = await fetchSearch(liteUrl); if (litePage && litePage.content) { const $lite = cheerio.load(litePage.content); links = extractLinks($lite, litePage.url, 'td.result-link > a, a.result-link, a.result__a').slice(0, maxResults); } } if (!links.length) { throw new Error(`No search results parsed for query "${query}".`); } // Convert to SearchResult format const results = links.map((link) => ({ title: link.text, url: link.href, snippet: '', // We could add snippet extraction here if needed source: new URL(link.href).hostname, })); return results; } function isNotNull(value) { return value !== null; } //# sourceMappingURL=web-search.js.map