Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 81 additions & 29 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
{
Expand Down Expand Up @@ -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: {
Expand All @@ -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'],
},
Expand All @@ -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: [
{
Expand All @@ -118,37 +186,21 @@ class WebSearchServer {
});
}

private async performSearch(query: string, limit: number): Promise<SearchResult[]> {
const response = await axios.get('https://www.google.com/search', {
params: { q: query },
private async performSearch(query: string, limit: number, engineName: string): Promise<SearchResult[]> {
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() {
Expand Down