From b0ef816bae5c19934b30285b622fcabf93bfd36a Mon Sep 17 00:00:00 2001 From: rahultah Date: Fri, 3 Oct 2025 00:28:07 +0530 Subject: [PATCH] feat: add multi-engine search support with DuckDuckGo - Add DuckDuckGo HTML search engine as default (more reliable than Google) - Keep Google as optional search engine for backward compatibility - Add configurable 'engine' parameter to search tool - Update TypeScript interfaces and validation - Improve error handling for invalid engine names - DuckDuckGo works better for web scraping (no JavaScript required) --- src/index.ts | 110 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 81 insertions(+), 29 deletions(-) diff --git a/src/index.ts b/src/index.ts index 76432c0..64901c0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,15 +16,77 @@ interface SearchResult { description: string; } -const isValidSearchArgs = (args: any): args is { query: string; limit?: number } => +interface SearchEngine { + name: string; + url: string; + parseResults: ($: any, limit: number) => SearchResult[]; +} + +const isValidSearchArgs = (args: any): args is { query: string; limit?: number; engine?: string } => typeof args === 'object' && args !== null && typeof args.query === 'string' && - (args.limit === undefined || typeof args.limit === 'number'); + (args.limit === undefined || typeof args.limit === 'number') && + (args.engine === undefined || typeof args.engine === 'string'); class WebSearchServer { private server: Server; + private searchEngines: SearchEngine[] = [ + { + name: 'duckduckgo', + url: 'https://html.duckduckgo.com/html/', + parseResults: ($, limit) => { + const results: SearchResult[] = []; + $('.result.results_links_deep').each((i: number, element: any) => { + if (i >= limit) return false; + + const titleElement = $(element).find('.result__title a'); + const urlElement = $(element).find('.result__title a'); + const snippetElement = $(element).find('.result__snippet'); + + if (titleElement.length && urlElement.length) { + const url = urlElement.attr('href'); + if (url && url.startsWith('http')) { + results.push({ + title: titleElement.text().trim(), + url: url, + description: snippetElement.text().trim() || '', + }); + } + } + }); + return results; + } + }, + { + name: 'google', + url: 'https://www.google.com/search', + parseResults: ($, limit) => { + const results: SearchResult[] = []; + $('div.g').each((i: number, element: any) => { + if (i >= limit) return false; + + const titleElement = $(element).find('h3'); + const linkElement = $(element).find('a'); + const snippetElement = $(element).find('.VwiC3b'); + + if (titleElement.length && linkElement.length) { + const url = linkElement.attr('href'); + if (url && url.startsWith('http')) { + results.push({ + title: titleElement.text(), + url: url, + description: snippetElement.text() || '', + }); + } + } + }); + return results; + } + } + ]; + constructor() { this.server = new Server( { @@ -52,7 +114,7 @@ class WebSearchServer { tools: [ { name: 'search', - description: 'Search the web using Google (no API key required)', + description: 'Search the web using multiple search engines (no API key required)', inputSchema: { type: 'object', properties: { @@ -66,6 +128,11 @@ class WebSearchServer { minimum: 1, maximum: 10, }, + engine: { + type: 'string', + description: 'Search engine to use (default: duckduckgo, options: duckduckgo, google)', + enum: ['duckduckgo', 'google'], + }, }, required: ['query'], }, @@ -90,9 +157,10 @@ class WebSearchServer { const query = request.params.arguments.query; const limit = Math.min(request.params.arguments.limit || 5, 10); + const engine = request.params.arguments.engine || 'duckduckgo'; try { - const results = await this.performSearch(query, limit); + const results = await this.performSearch(query, limit, engine); return { content: [ { @@ -118,37 +186,21 @@ class WebSearchServer { }); } - private async performSearch(query: string, limit: number): Promise { - const response = await axios.get('https://www.google.com/search', { - params: { q: query }, + private async performSearch(query: string, limit: number, engineName: string): Promise { + const engine = this.searchEngines.find(e => e.name === engineName); + if (!engine) { + throw new Error(`Search engine '${engineName}' not found. Available engines: ${this.searchEngines.map(e => e.name).join(', ')}`); + } + + const response = await axios.get(engine.url, { + params: engine.name === 'duckduckgo' ? { q: query } : { q: query }, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' } }); const $ = cheerio.load(response.data); - const results: SearchResult[] = []; - - $('div.g').each((i, element) => { - if (i >= limit) return false; - - const titleElement = $(element).find('h3'); - const linkElement = $(element).find('a'); - const snippetElement = $(element).find('.VwiC3b'); - - if (titleElement.length && linkElement.length) { - const url = linkElement.attr('href'); - if (url && url.startsWith('http')) { - results.push({ - title: titleElement.text(), - url: url, - description: snippetElement.text() || '', - }); - } - } - }); - - return results; + return engine.parseResults($, limit); } async run() {