Skip to main content
Glama
brendon92

Specialized AI Search Tools

by brendon92

websearch

websearch

Search the web across multiple engines with advanced filters for file type, language, region, date range, and domains to find specific information.

Instructions

Search the web using various search engines (DuckDuckGo, Brave, Google, Bing, SerpAPI) with advanced filtering options. Returns a list of search results with titles, URLs, and snippets.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
queryYesSearch query string
engineNoSearch engine to useduckduckgo
limitNoMaximum number of results
filtersNo

Implementation Reference

  • The protected execute method implements the core logic of the websearch tool, handling input parameters and dispatching to specific search engine implementations based on the 'engine' parameter.
    protected async execute(params: WebSearchParams): Promise<SearchResult[]> {
        logger.info(`Searching with ${params.engine}`, { query: params.query, limit: params.limit });
    
        try {
            switch (params.engine) {
                case 'duckduckgo':
                    return await this.searchDuckDuckGo(params);
                case 'brave':
                    return await this.searchBrave(params);
                case 'google':
                    return await this.searchGoogle(params);
                case 'bing':
                    return await this.searchBing(params);
                case 'serpapi':
                    return await this.searchSerpApi(params);
                default:
                    throw new Error(`Unsupported search engine: ${params.engine}`);
            }
        } catch (error) {
            logger.error(`Search failed for ${params.engine}`, { error });
            throw new Error(
                `Search failed: ${error instanceof Error ? error.message : 'Unknown error'}`
            );
        }
    }
  • Defines the input schema (webSearchSchema) for the websearch tool using Zod, including query, engine selection, result limit, and optional filters.
    const webSearchSchema = z.object({
        query: z.string().min(1).describe('Search query string'),
        engine: z
            .enum(['duckduckgo', 'brave', 'google', 'bing', 'serpapi'])
            .optional()
            .default('duckduckgo')
            .describe('Search engine to use'),
        limit: z.number().int().min(1).max(50).optional().default(10).describe('Maximum number of results'),
        filters: searchFiltersSchema,
    });
  • Registers the WebSearchTool instance (new WebSearchTool()) along with other tools to the MCP server via server.registerTools().
    export function registerAllTools(server: MCPServer): void {
        const tools: BaseTool[] = [
            new WebSearchTool(),
            new WebFetchTool(),
            new TypeConversionTool(),
        ];
    
        // Register all tools
        if (tools.length > 0) {
            server.registerTools(tools);
            logger.info(`Registered ${tools.length} tool(s): ${tools.map(t => t.name).join(', ')}`);
        } else {
            logger.warn('No tools registered - add tool implementations to src/tools/index.ts');
        }
    }
  • Supporting schema for search filters used within the websearch tool's main schema.
    const searchFiltersSchema = z
        .object({
            fileType: z.string().optional().describe('Filter by file type (e.g., pdf, doc, xls)'),
            language: z.string().optional().describe('Filter by language code (e.g., en, pl, de)'),
            region: z.string().optional().describe('Filter by region/country code (e.g., us, pl, uk)'),
            dateRange: z
                .enum(['day', 'week', 'month', 'year'])
                .optional()
                .describe('Filter by date range'),
            domain: z.string().optional().describe('Filter by specific domain (e.g., github.com)'),
            exactPhrase: z.boolean().optional().describe('Search for exact phrase match'),
        })
        .optional();
  • The WebSearchTool class definition, including name, description, schema assignment, execute handler, and private helper methods for different search engines.
    export class WebSearchTool extends BaseTool<typeof webSearchSchema> {
        readonly name = 'websearch';
        readonly description =
            'Search the web using various search engines (DuckDuckGo, Brave, Google, Bing, SerpAPI) with advanced filtering options. Returns a list of search results with titles, URLs, and snippets.';
        readonly schema = webSearchSchema;
    
        protected async execute(params: WebSearchParams): Promise<SearchResult[]> {
            logger.info(`Searching with ${params.engine}`, { query: params.query, limit: params.limit });
    
            try {
                switch (params.engine) {
                    case 'duckduckgo':
                        return await this.searchDuckDuckGo(params);
                    case 'brave':
                        return await this.searchBrave(params);
                    case 'google':
                        return await this.searchGoogle(params);
                    case 'bing':
                        return await this.searchBing(params);
                    case 'serpapi':
                        return await this.searchSerpApi(params);
                    default:
                        throw new Error(`Unsupported search engine: ${params.engine}`);
                }
            } catch (error) {
                logger.error(`Search failed for ${params.engine}`, { error });
                throw new Error(
                    `Search failed: ${error instanceof Error ? error.message : 'Unknown error'}`
                );
            }
        }
    
        /**
         * Search using DuckDuckGo (free, no API key required)
         */
        private async searchDuckDuckGo(params: WebSearchParams): Promise<SearchResult[]> {
            let query = params.query;
    
            // Apply filters to query
            if (params.filters?.exactPhrase) {
                query = `"${query}"`;
            }
            if (params.filters?.fileType) {
                query += ` filetype:${params.filters.fileType}`;
            }
            if (params.filters?.domain) {
                query += ` site:${params.filters.domain}`;
            }
    
            const url = 'https://html.duckduckgo.com/html/';
            const response = await httpClient.post(
                url,
                new URLSearchParams({
                    q: query,
                    kl: params.filters?.region || '',
                }),
                {
                    headers: {
                        'Content-Type': 'application/x-www-form-urlencoded',
                    },
                }
            );
    
            const $ = cheerio.load(response.data);
            const results: SearchResult[] = [];
    
            $('.result').each((_, element): false | void => {
                if (results.length >= params.limit) return false;
    
                const $result = $(element);
                const $title = $result.find('.result__title');
                const $snippet = $result.find('.result__snippet');
    
                const title = $title.text().trim();
                const snippet = $snippet.text().trim();
                const url = $title.find('a').attr('href') || '';
    
                if (title && url) {
                    results.push({
                        title,
                        url: this.cleanDuckDuckGoUrl(url),
                        snippet,
                        source: 'duckduckgo',
                    });
                }
            });
    
            logger.info(`DuckDuckGo search completed`, { resultsFound: results.length });
            return results;
        }
    
        /**
         * Search using Brave Search API
         */
        private async searchBrave(params: WebSearchParams): Promise<SearchResult[]> {
            const apiKey = process.env.BRAVE_API_KEY;
            if (!apiKey) {
                throw new Error('BRAVE_API_KEY environment variable is required for Brave Search');
            }
    
            let query = params.query;
            if (params.filters?.exactPhrase) {
                query = `"${query}"`;
            }
            if (params.filters?.fileType) {
                query += ` filetype:${params.filters.fileType}`;
            }
            if (params.filters?.domain) {
                query += ` site:${params.filters.domain}`;
            }
    
            const url = 'https://api.search.brave.com/res/v1/web/search';
            const response = await httpClient.get(url, {
                params: {
                    q: query,
                    count: params.limit,
                    country: params.filters?.region?.toUpperCase(),
                },
                headers: {
                    'X-Subscription-Token': apiKey,
                    Accept: 'application/json',
                },
            });
    
            const results: SearchResult[] = (response.data.web?.results || []).map((item: any) => ({
                title: item.title,
                url: item.url,
                snippet: item.description || '',
                source: 'brave',
            }));
    
            logger.info(`Brave search completed`, { resultsFound: results.length });
            return results;
        }
    
        /**
         * Search using Google Custom Search API
         */
        private async searchGoogle(params: WebSearchParams): Promise<SearchResult[]> {
            const apiKey = process.env.GOOGLE_API_KEY;
            const searchEngineId = process.env.GOOGLE_SEARCH_ENGINE_ID;
    
            if (!apiKey || !searchEngineId) {
                throw new Error(
                    'GOOGLE_API_KEY and GOOGLE_SEARCH_ENGINE_ID environment variables are required for Google Search'
                );
            }
    
            let query = params.query;
            if (params.filters?.exactPhrase) {
                query = `"${query}"`;
            }
            if (params.filters?.fileType) {
                query += ` filetype:${params.filters.fileType}`;
            }
            if (params.filters?.domain) {
                query += ` site:${params.filters.domain}`;
            }
    
            const url = 'https://www.googleapis.com/customsearch/v1';
            const response = await httpClient.get(url, {
                params: {
                    key: apiKey,
                    cx: searchEngineId,
                    q: query,
                    num: Math.min(params.limit, 10), // Google API max is 10 per request
                    lr: params.filters?.language ? `lang_${params.filters.language}` : undefined,
                    gl: params.filters?.region,
                    dateRestrict: this.getGoogleDateRestrict(params.filters?.dateRange),
                },
            });
    
            const results: SearchResult[] = (response.data.items || []).map((item: any) => ({
                title: item.title,
                url: item.link,
                snippet: item.snippet || '',
                source: 'google',
            }));
    
            logger.info(`Google search completed`, { resultsFound: results.length });
            return results;
        }
    
        /**
         * Search using Bing Search API
         */
        private async searchBing(params: WebSearchParams): Promise<SearchResult[]> {
            const apiKey = process.env.BING_API_KEY;
            if (!apiKey) {
                throw new Error('BING_API_KEY environment variable is required for Bing Search');
            }
    
            let query = params.query;
            if (params.filters?.exactPhrase) {
                query = `"${query}"`;
            }
            if (params.filters?.fileType) {
                query += ` filetype:${params.filters.fileType}`;
            }
            if (params.filters?.domain) {
                query += ` site:${params.filters.domain}`;
            }
    
            const url = 'https://api.bing.microsoft.com/v7.0/search';
            const response = await httpClient.get(url, {
                params: {
                    q: query,
                    count: params.limit,
                    mkt: params.filters?.region || 'en-US',
                    freshness: this.getBingFreshness(params.filters?.dateRange),
                },
                headers: {
                    'Ocp-Apim-Subscription-Key': apiKey,
                },
            });
    
            const results: SearchResult[] = (response.data.webPages?.value || []).map((item: any) => ({
                title: item.name,
                url: item.url,
                snippet: item.snippet || '',
                source: 'bing',
            }));
    
            logger.info(`Bing search completed`, { resultsFound: results.length });
            return results;
        }
    
        /**
         * Clean DuckDuckGo redirect URLs
         */
        private cleanDuckDuckGoUrl(url: string): string {
            try {
                const urlObj = new URL(url, 'https://duckduckgo.com');
                const uddg = urlObj.searchParams.get('uddg');
                return uddg || url;
            } catch {
                return url;
            }
        }
    
        /**
         * Convert date range to Google dateRestrict format
         */
        private getGoogleDateRestrict(dateRange?: string): string | undefined {
            if (!dateRange) return undefined;
            const map: Record<string, string> = {
                day: 'd1',
                week: 'w1',
                month: 'm1',
                year: 'y1',
            };
            return map[dateRange];
        }
    
        /**
         * Convert date range to Bing freshness format
         */
        private getBingFreshness(dateRange?: string): string | undefined {
            if (!dateRange) return undefined;
            const map: Record<string, string> = {
                day: 'Day',
                week: 'Week',
                month: 'Month',
                year: 'Year',
            };
            return map[dateRange];
        }
    
        /**
         * Search using SerpAPI (supports multiple search engines)
         */
        private async searchSerpApi(params: WebSearchParams): Promise<SearchResult[]> {
            const apiKey = process.env.SERP_API_KEY;
            if (!apiKey) {
                throw new Error('SERP_API_KEY environment variable is required for SerpAPI');
            }
    
            let query = params.query;
            if (params.filters?.exactPhrase) {
                query = `"${query}"`;
            }
            if (params.filters?.fileType) {
                query += ` filetype:${params.filters.fileType}`;
            }
            if (params.filters?.domain) {
                query += ` site:${params.filters.domain}`;
            }
    
            const url = 'https://serpapi.com/search';
            const response = await httpClient.get(url, {
                params: {
                    api_key: apiKey,
                    q: query,
                    num: params.limit,
                    engine: 'google', // SerpAPI supports multiple engines, defaulting to google
                    hl: params.filters?.language || 'en',
                    gl: params.filters?.region || 'us',
                    tbs: this.getSerpApiDateFilter(params.filters?.dateRange),
                },
            });
    
            const results: SearchResult[] = (response.data.organic_results || []).map((item: any) => ({
                title: item.title,
                url: item.link,
                snippet: item.snippet || '',
                source: 'serpapi',
            }));
    
            logger.info(`SerpAPI search completed`, { resultsFound: results.length });
            return results.slice(0, params.limit);
        }
    
        /**
         * Convert date range to SerpAPI tbs format
         */
        private getSerpApiDateFilter(dateRange?: string): string | undefined {
            if (!dateRange) return undefined;
            const map: Record<string, string> = {
                day: 'qdr:d',
                week: 'qdr:w',
                month: 'qdr:m',
                year: 'qdr:y',
            };
            return map[dateRange];
        }
    }
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It states what the tool does and returns ('Returns a list of search results with titles, URLs, and snippets'), but lacks details on rate limits, authentication needs, error handling, or performance characteristics. It mentions 'advanced filtering options' but doesn't explain behavioral implications like how filters affect results or latency.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with the core purpose in the first sentence and efficiently adds details in the second sentence without redundancy. Every sentence earns its place by conveying essential information about functionality and output, making it appropriately sized and well-structured.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the complexity (4 parameters with nested objects, no output schema, and no annotations), the description is adequate but has gaps. It covers the basic purpose and output format, but lacks details on error cases, pagination, or how the tool integrates with the search engines listed. Without annotations or output schema, more behavioral context would improve completeness for this multi-engine search tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 75%, so the schema documents most parameters well. The description adds value by summarizing the purpose ('Search the web using various search engines... with advanced filtering options') and hinting at the 'filters' parameter's role, but doesn't provide additional semantic context beyond what the schema already covers for individual parameters like 'query' or 'engine'.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the specific action ('Search the web') with the resource ('using various search engines') and distinguishes it from sibling tools like 'typeconversion' and 'webfetch' by focusing on search functionality rather than conversion or fetching. It explicitly mentions the verb 'Search' and the scope 'with advanced filtering options'.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies usage for web searching with filtering, but provides no explicit guidance on when to use this tool versus alternatives like 'webfetch' (which might retrieve specific URLs) or other search methods. It mentions 'various search engines' but doesn't specify when to choose one over another or any exclusions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/brendon92/mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server