Skip to main content
Glama
guptabhishek

Multi-Search MCP Server

by guptabhishek

search

Retrieve web results from multiple search engines with automatic fallback between Google, Tavily, DuckDuckGo, and Brave.

Instructions

Performs a web search using multiple providers (google, tavily, duckduckgo, brave).

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
queryYesThe search query.
providerNoOptional: Specify a provider directly (google, tavily, duckduckgo, brave). If omitted, uses priority fallback.
num_resultsNoNumber of results desired (default: 10).

Implementation Reference

  • src/index.ts:15-43 (registration)
    The tool name constant 'search' and its full schema definition including name, description, and inputSchema with parameters (query, provider, num_results).
    const TOOL_NAME = 'search';
    
    const TOOL_DEFINITION = {
      name: TOOL_NAME,
      description: 'Performs a web search using multiple providers (google, tavily, duckduckgo, brave).',
      inputSchema: {
        type: 'object',
        properties: {
          query: {
            type: 'string',
            description: 'The search query.',
          },
          provider: {
            type: 'string',
            enum: ['google', 'tavily', 'duckduckgo', 'brave'],
            description: 'Optional: Specify a provider directly (google, tavily, duckduckgo, brave). If omitted, uses priority fallback.',
          },
          num_results: {
            type: 'integer',
            description: `Number of results desired (default: ${DEFAULT_NUM_RESULTS}).`,
            default: DEFAULT_NUM_RESULTS,
            minimum: 1,
            maximum: 20,
          },
        },
        required: ['query'],
        additionalProperties: false,
      },
    };
  • The main handler for the 'search' tool. Handles validation, provider selection (priority/random/requested), fallback logic, API key checks, calling search functions with timeout, result standardization, and formatting the MCP response.
    this.server.setRequestHandler(CallToolRequestSchema, async (request: any) => {
      if (request.params.name !== TOOL_NAME) {
        throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
      }
      const args = request.params.arguments;
      // Validate input
      if (!args.query || typeof args.query !== 'string' || args.query.trim() === '') {
        throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid required argument: query (string)');
      }
      const query = args.query;
      const requestedProvider = args.provider;
      const numResults = args.num_results || DEFAULT_NUM_RESULTS;
    
      if (requestedProvider && !searchFunctions[requestedProvider]) {
        throw new McpError(ErrorCode.InvalidParams, `Invalid provider specified: ${requestedProvider}. Valid options are: google, tavily`);
      }
    
      if (requestedProvider === 'tavily') {
        if (!API_KEYS.tavily_key) {
          // Throw as InvalidParams because it's a configuration issue tied to the specific request
          throw new McpError(ErrorCode.InvalidParams, `Tavily API key is not configured for this server`);
        }
      }
       if (requestedProvider === 'google') {
           if (!API_KEYS.google_key || !API_KEYS.google_cx) {
             // Throw as InvalidParams because it's a configuration issue tied to the specific request
             throw new McpError(ErrorCode.InvalidParams, `Google API key or CX are not configured for this server`);
           }
         }
    
    
      // --- Start of logic to track fallback and order ---
      let providersAttemptedOrder: string[];
      if (requestedProvider) {
        providersAttemptedOrder = [requestedProvider];
      } else if (SEARCH_STRATEGY === 'random') {
        // Pick a random provider from the enabled list
        const enabled = PROVIDER_LIST.filter(p => searchFunctions[p]);
        if (enabled.length === 0) {
          throw new McpError(ErrorCode.InternalError, 'No enabled providers available for random selection.');
        }
        const randomProvider = enabled[Math.floor(Math.random() * enabled.length)];
        providersAttemptedOrder = [randomProvider];
      } else {
        // Default: priority order (try all in order)
        providersAttemptedOrder = PROVIDER_LIST.filter(p => searchFunctions[p]);
      }
      // Fallback is considered triggered if no specific provider was requested,
      // meaning the server attempted providers based on the configured list.
      const fallbackTriggered = !requestedProvider;
      // --- End of logic to track fallback and order ---
    
      // Provider execution logic
      let finalResults: StandardizedSearchResult[] = [];
      let errorMessages: string[] = [];
      let successfulProvider: string | null = null;
      const timeoutMs = 30000; // 15 second timeout per provider
    
      for (const provider of providersAttemptedOrder) { // Use the list captured for tracking
        try {
          const searchFn = searchFunctions[provider];
          const standardizeFn = standardizeFunctions[provider];
    
          // --- Move API key check here, inside the loop before attempting the provider ---
          // This ensures we only throw missing key errors for providers we actually try
          // and prevents the loop from stopping if the *first* provider in the priority list
          // has a missing key but later ones might be configured.
          switch (provider) {
             case 'google':
               if (!API_KEYS.google_key || !API_KEYS.google_cx) {
                 // Throw a standard Error here, which will be caught by the inner try/catch
                 throw new Error('Google API key or CX missing');
               }
               break;
             case 'tavily':
               if (!API_KEYS.tavily_key) {
                 // Throw a standard Error here
                 throw new Error('Tavily API key missing');
               }
               break;
             case 'duckduckgo':
               // DuckDuckGo does not require an API key
               break;
             case 'brave':
               if (!API_KEYS.brave_key) {
                 throw new Error('Brave Search API key missing');
               }
               break;
           }
            // --- End of moved API key check ---
    
           // Prepare provider-specific arguments
           let providerArgs: any[];
           switch (provider) {
             case 'google':
               providerArgs = [API_KEYS.google_key, API_KEYS.google_cx, numResults];
               break;
             case 'tavily':
               providerArgs = [API_KEYS.tavily_key, numResults];
               break;
             case 'duckduckgo':
               providerArgs = [numResults];
               break;
             case 'brave':
               providerArgs = [API_KEYS.brave_key, numResults];
               break;
             default:
               // Should not happen due to earlier checks, but good practice
               throw new McpError(ErrorCode.InternalError, `Internal error: Unknown provider ${provider}`);
           }
    
          // Execute search with timeout
          const rawResults = await Promise.race([
            searchFn(query, ...providerArgs),
            new Promise((_, reject) => setTimeout(() => reject(new Error(`${provider} search timed out after ${timeoutMs}ms`)), timeoutMs)),
          ]);
    
          // Standardize results, passing the provider name
          // Add type assertion for rawResults from Promise.race
          const standardized = standardizeFn(rawResults as any[], provider);
    
          if (standardized.length > 0) {
            finalResults = standardized;
            successfulProvider = provider;
            break; // Exit loop on first success
          } else {
            console.error(`${provider} returned 0 results.`);
            // Don't add to errorMessages here, it's not an error, just no results
          }
        } catch (error: any) {
          // Catch errors thrown by providers (missing keys, API errors, timeouts)
          const message = `${provider}: ${error.message}`;
          console.error(`Error during search with ${provider}:`, error.message);
          errorMessages.push(message);
        }
      }
    
      // Check if any provider succeeded
      if (successfulProvider && finalResults.length > 0) {
        // Format results according to MCP spec (array of content items)
        const content = finalResults.map(item => ({
          type: 'text', // Use 'text' type for the search results
          // Combine the details into a single text string for the 'text' content item
          text: `Title: ${item.title}\nLink: ${item.link}\nSnippet: ${item.snippet}\nSource: ${item.provider}`,
        }));
    
        // --- Construct the 'data' object with fallback information ---
        const responseData = {
           fallbackTriggered: fallbackTriggered,
           providersAttemptedOrder: providersAttemptedOrder,
           successfulProvider: successfulProvider, // Include the successful provider for clarity
           errorsDuringAttempt: errorMessages.length > 0 ? errorMessages : undefined // Optionally include errors if any occurred before success
        };
        // --- End of 'data' object construction ---
    
    
        // Return MCP success response including content AND data
        return { content, data: responseData };
      } else {
        // If no provider succeeded, throw an MCP error
        const combinedErrors = errorMessages.join('; ');
        console.error(`Search failed for query "${query}". Errors: ${combinedErrors}`);
        // You could also include the attempted providers list in the error data if needed
        // const errorData = { providersAttemptedOrder };
        throw new McpError(
          ErrorCode.InternalError, // Or a more specific code if applicable
          `Search failed. No provider succeeded for query "${query}". Errors: ${combinedErrors || 'No results found or provider configuration issue.'}`,
         // errorData // Optional: pass errorData here
        );
      }
    });
  • Tavily search provider implementation. Calls the Tavily API with the query and API key, returns simplified search results.
    export async function searchTavily(query: string, apiKey: string, numResults: number): Promise<SimplifiedSearchResult[]> {
      if (!apiKey) {
        // This check is also in the server handler, but good to have here too
        throw new Error('Tavily API key is missing');
      }
    
      const baseUrl = 'https://api.tavily.com/search';
    
      try {
        const response = await axios.post(baseUrl, {
          query: query,
          max_results: numResults, // Use max_results parameter
          include_answer: "basic", // Keep if you want the answer field (separate from results array)
          // include_images: false, // Add options as needed
          // include_deeplinks: true,
        }, {
          headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${apiKey}`
          }
        });
    
        // Check if response data or results array is valid
        if (!response.data || !Array.isArray(response.data.results)) {
             console.error('Tavily API returned unexpected response format:', response.data);
             const errorDetails = response.data ? JSON.stringify(response.data).substring(0, 200) + '...' : 'No response data';
             throw new Error(`Tavily API returned unexpected response format: ${errorDetails}`);
        }
    
        // Map the raw Tavily results to the simplified structure
        // Filtering for valid results (e.g., missing title/link/content) is better done in the standardize step
        // This mapping just assumes the basic structure exists and provides defaults
        return response.data.results.map((item: any) => ({
          title: item.title || '',
          link: item.url || '',   
          content: item.content || '',
        })).filter((item: SimplifiedSearchResult) => item.title && item.link && item.content); // Optional: Filter out results with no title, link, or content
    
    
      } catch (error: any) {
        console.error('Tavily Search API Error:', error.message);
        throw error;
      }
    }
  • Brave Search provider implementation. Calls the Brave Search API with the query and API key, extracts web results.
    export async function searchBrave(query: string, apiKey: string, numResults: number): Promise<any[]> {
      if (!apiKey) {
        throw new Error('Brave Search API key is missing');
      }
    
      const baseUrl = 'https://api.search.brave.com/res/v1/web/search';
      // Supported params: q, safesearch, spellcheck, country, search_lang, freshness, sources, extra_snippets, ai_summarization, goggles, page, use_autopaging
      const params: Record<string, any> = {
        q: query,
        count: numResults
        // Add more supported params here if needed
      };
    
      try {
        const response = await axios.get(baseUrl, {
          params,
          headers: {
            'X-Subscription-Token': apiKey,
            'Accept': 'application/json',
            'Accept-Encoding': 'gzip',
          },
        });
    
        const data: any = response.data;
        // Brave API: Only extract text (web) results, ignore videos and mixed
        if (data && data.web && Array.isArray(data.web.results)) {
          return data.web.results;
        }
        // Optionally handle locations if needed, else just return []
        return [];
      } catch (error: any) {
        console.error('Brave Search API Error:', error.message);
        throw new Error(`Brave Search API Error: ${error.message}`);
      }
    }
  • StandardizedSearchResult interface and four standardize functions (Google, Tavily, DuckDuckGo, Brave) that transform raw provider results into a uniform format with title, link, snippet, and provider fields.
    export interface StandardizedSearchResult {
      title: string;
      link: string;
      snippet: string; // Represents the description/content snippet
      provider: string;
    }
    
    export function standardizeGoogleResults(rawResults: RawGoogleResult[], providerName: string): StandardizedSearchResult[] {
      // Filter out results missing essential fields like snippet, title, or link
      const filteredResults = rawResults.filter(item =>
          item.title && item.link && typeof item.snippet === 'string' && item.snippet.length > 0
      );
    
      return filteredResults.map((item: RawGoogleResult) => ({
        title: item.title,
        link: item.link,
        snippet: item.snippet,
        provider: providerName,
      }));
    }
    
    
    export function standardizeTavilyResults(rawResults: RawTavilyResult[], providerName: string): StandardizedSearchResult[] {
        // Filter out results missing essential fields
        // Note: Tavily provider already maps raw results to { title, link, content }
         const filteredResults = rawResults.filter(item =>
            item.title && item.link && typeof item.content === 'string' && item.content.length > 0
        );
    
      return filteredResults.map((item: RawTavilyResult) => ({
        title: item.title,
        link: item.link,
        snippet: item.content, // Use 'content' as the snippet
        provider: providerName,
      }));
    }
    
    interface RawDuckDuckGoResult {
      title: string;
      link: string;
      snippet: string;
    }
    
    export function standardizeDuckDuckGoResults(rawResults: RawDuckDuckGoResult[], providerName: string): StandardizedSearchResult[] {
      const filteredResults = rawResults.filter(item =>
        item.title && item.link && typeof item.snippet === 'string' && item.snippet.length > 0
      );
    
      return filteredResults.map((item: RawDuckDuckGoResult) => ({
        title: item.title,
        link: item.link,
        snippet: item.snippet,
        provider: providerName,
      }));
    }
    
    interface RawBraveResult {
      title: string;
      url: string;
      description: string;
    }
    
    export function standardizeBraveResults(rawResults: RawBraveResult[], providerName: string): StandardizedSearchResult[] {
      const filteredResults = rawResults.filter(item =>
        item.title && item.url && typeof item.description === 'string' && item.description.length > 0
      );
    
      return filteredResults.map((item: RawBraveResult) => ({
        title: item.title,
        link: item.url,
        snippet: item.description,
        provider: providerName,
      }));
    }
Behavior2/5

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

No annotations are provided, and the description lacks behavioral details such as how results are returned, how provider fallback works, rate limits, or authentication requirements. This leaves the agent with insufficient context for safe operation.

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?

A single sentence of 11 words that front-loads the action ('Performs a web search') and includes key details, achieving maximum efficiency with no redundancy.

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

Completeness2/5

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

For a tool with no output schema and no annotations, the description omits critical information: expected output format, response structure, error behavior, and how provider priority fallback works. This hinders effective agent invocation.

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

Parameters3/5

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

Schema coverage is 100% with descriptions for all parameters. The description adds minimal value—it lists providers, but the schema already does so via enum. No additional semantics about parameter usage.

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 tool performs a web search and lists the supported providers, which is specific and distinguishes it from potential tools with different search capabilities.

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 the tool is for web searches but does not provide explicit guidance on when to use this tool versus alternatives (though there are no siblings), nor does it mention prerequisites or when not to use.

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/guptabhishek/multi-search-mcp'

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