Skip to main content
Glama

search

Execute web searches via public SearXNG instances, returning structured results with URLs and summaries. Filter by time range, language, or enable detailed searches for comprehensive query responses.

Instructions

Performs a web search for a given query using the public SearXNG search servers. Returns an array of result objects with 'url' and 'summary' for each result.

Input Schema

NameRequiredDescriptionDefault
detailedNoOptionally, if true, will perform a more thorough search - will ask for more pages of results and will merge results from multiple servers. Warning: this might overload the servers and cause errors. Do not set to true by default unless explicitly asked to perform a detailed or comprehensive query.
languageNoThe optional language code for the search (e.g., en, es, fr).
queryYesThe search query.
time_rangeNoThe optional time range for the search, from: [day, week, month, year].

Input Schema (JSON Schema)

{ "$schema": "http://json-schema.org/draft-07/schema#", "additionalProperties": false, "properties": { "detailed": { "description": "Optionally, if true, will perform a more thorough search - will ask for more pages of results and will merge results from multiple servers. Warning: this might overload the servers and cause errors. Do not set to true by default unless explicitly asked to perform a detailed or comprehensive query.", "type": "string" }, "language": { "description": "The optional language code for the search (e.g., en, es, fr).", "type": "string" }, "query": { "description": "The search query.", "type": "string" }, "time_range": { "description": "The optional time range for the search, from: [day, week, month, year].", "type": "string" } }, "required": [ "query" ], "type": "object" }

Implementation Reference

  • The main execution handler for the 'search' tool. It destructures input parameters, validates the base URL environment variable, and conditionally performs a detailed search (multiple servers and pages, merging unique results) or a standard search with retry logic using helper functions.
    execute: async (params, { log }) => { const { query, time_range, language, detailed } = params; if (baseUrl === undefined || baseUrl.length === 0) { throw new UserError('SEARXNG_BASE_URL environment variable is not set.'); } // If detailed search is requested if (detailed === 'true') { const shuffledUrls = shuffleAndFilterUrls(baseUrl); // Use up to 3 servers for detailed search const serversToUse = shuffledUrls; const allResults: { url: string; summary: string }[] = []; const processedUrls = new Set<string>(); let successfulServers = 0; // Fetch results from each server for (const serverUrl of serversToUse) { if (successfulServers >= 3) { break; } try { // Fetch multiple pages of results const serverResults = await fetchMultiplePages(log, query, serverUrl, 3, time_range, language); addUniqueResults(allResults, serverResults, processedUrls); if (serverResults.length > 0) { successfulServers++; } } catch (error) { log.error('Error fetching results from server', { serverUrl, error: error instanceof Error ? error.message : String(error) }); } } return { content: [{ type: 'text', text: JSON.stringify(allResults) }], }; } else { // Standard search (existing behavior) const shuffledUrls = shuffleAndFilterUrls(baseUrl); const response = await fetchWithRetry(log, query, shuffledUrls, 5, time_range, language); if (response) { return response; } else { throw new UserError('No valid response received after multiple attempts.'); } } },
  • Zod schema defining the input parameters for the 'search' tool: required 'query', optional 'time_range', 'language', and 'detailed'.
    parameters: z.object({ query: z.string({ description: 'The search query.' }), time_range: z.string({ description: 'The optional time range for the search, from: [day, week, month, year].' }).optional(), language: z.string({ description: 'The optional language code for the search (e.g., en, es, fr).' }).optional(), detailed: z.string({ description: 'Optionally, if true, will perform a more thorough search - will ask for more pages of results and will merge results from multiple servers. Warning: this might overload the servers and cause errors. Do not set to true by default unless explicitly asked to perform a detailed or comprehensive query.'}).optional() }),
  • src/index.ts:222-284 (registration)
    Registers the 'search' tool with the FastMCP server instance, specifying name, description, input schema, annotations, and the execute handler function.
    server.addTool({ name: 'search', description: 'Performs a web search for a given query using the public SearXNG search servers. Returns an array of result objects with \'url\' and \'summary\' for each result.', parameters: z.object({ query: z.string({ description: 'The search query.' }), time_range: z.string({ description: 'The optional time range for the search, from: [day, week, month, year].' }).optional(), language: z.string({ description: 'The optional language code for the search (e.g., en, es, fr).' }).optional(), detailed: z.string({ description: 'Optionally, if true, will perform a more thorough search - will ask for more pages of results and will merge results from multiple servers. Warning: this might overload the servers and cause errors. Do not set to true by default unless explicitly asked to perform a detailed or comprehensive query.'}).optional() }), annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true, idempotentHint: false }, execute: async (params, { log }) => { const { query, time_range, language, detailed } = params; if (baseUrl === undefined || baseUrl.length === 0) { throw new UserError('SEARXNG_BASE_URL environment variable is not set.'); } // If detailed search is requested if (detailed === 'true') { const shuffledUrls = shuffleAndFilterUrls(baseUrl); // Use up to 3 servers for detailed search const serversToUse = shuffledUrls; const allResults: { url: string; summary: string }[] = []; const processedUrls = new Set<string>(); let successfulServers = 0; // Fetch results from each server for (const serverUrl of serversToUse) { if (successfulServers >= 3) { break; } try { // Fetch multiple pages of results const serverResults = await fetchMultiplePages(log, query, serverUrl, 3, time_range, language); addUniqueResults(allResults, serverResults, processedUrls); if (serverResults.length > 0) { successfulServers++; } } catch (error) { log.error('Error fetching results from server', { serverUrl, error: error instanceof Error ? error.message : String(error) }); } } return { content: [{ type: 'text', text: JSON.stringify(allResults) }], }; } else { // Standard search (existing behavior) const shuffledUrls = shuffleAndFilterUrls(baseUrl); const response = await fetchWithRetry(log, query, shuffledUrls, 5, time_range, language); if (response) { return response; } else { throw new UserError('No valid response received after multiple attempts.'); } } }, });
  • Key helper function that fetches and parses search results from a single SearXNG server instance. Handles URL construction, anti-bot evasion by fetching client CSS, HTML parsing for results using regex on <article class="result"> blocks.
    export async function fetchResults(log: Log, query: string, baseUrl: string, time_range?: string, language?: string, page?: number, doNotRetryAgain?: boolean): Promise<ContentResult> { if (!baseUrl) { throw new UserError('Base URL not provided!'); } // Construct URL without format=json const url = `${baseUrl}/search?q=${encodeURIComponent(query)}${time_range ? `&time_range=${time_range}` : ''}${language ? `&language=${language}` : ''}${page && page > 1 ? `&pageno=${page}` : ''}`; try { log.debug('Fetching results from SearXNG', { url }); let response; try { // First fetch the base URL to get the main page response = await fetch(baseUrl, { method: 'GET', headers: { 'User-Agent': 'mcp-searxng-public/' + version } } ); // Get the HTML content to find the client CSS file const html = await response.text(); // Look for the client CSS file in the HTML const cssLinkMatch = html.match(/<link[^>]*rel=["']stylesheet["'][^>]*href=["'][^"']*\/client[^"']*\.css["'][^>]*>/i); if (cssLinkMatch) { const cssHrefMatch = cssLinkMatch[0].match(/href=["']([^"']*)["']/i); if (cssHrefMatch && cssHrefMatch[1]) { const cssUrl = new URL(cssHrefMatch[1], baseUrl).href; log.debug('Found client CSS file, fetching it', { cssUrl }); // Fetch the client CSS file try { await fetch(cssUrl, { method: 'GET', headers: { 'Referer': baseUrl, 'User-Agent': 'mcp-searxng-public/' + version } }); } catch (cssError) { log.warn('Failed to fetch client CSS file, continuing anyway', { cssError: cssError instanceof Error ? cssError.message : String(cssError) }); } } } await new Promise((resolve) => setTimeout(resolve, randomInt(10, 400))); response = await fetch(url, { method: 'GET', headers: { 'Referer': baseUrl, 'User-Agent': 'mcp-searxng-public/' + version } }); } catch (error) { log.error('Error fetching results from SearXNG', { error: error instanceof Error ? error.message : String(error) }); } if (response === undefined || !response.ok) { throw new UserError(`HTTP error! Response: ${JSON.stringify(response ?? 'undefined')}`); } const html = await response.text(); if (html.includes("body class=\"index_endpoint\"")) { // We were thrown to the main page, throw an error and force retry if (doNotRetryAgain) { throw new UserError("Redirected to index page"); } else { await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait for 2 seconds before retrying return await fetchResults(log, query, baseUrl, time_range, language, page, true); } } // Basic HTML parsing to find result blocks, URLs, and summaries. // This is a naive approach and may not work for all SearXNG instances // due to variations in HTML structure. A proper HTML parser would be more robust. const resultsArray: { url: string; summary: string }[] = []; // Corrected regex to use standard HTML tags instead of escaped ones // Updated regex to match article tags with class 'result' const resultBlockRegex = /<article[^>]*class=["'][^"']*result[^"']*["'][^>]*>(.*?)<\/article>/gis; let blockMatch; while ((blockMatch = resultBlockRegex.exec(html)) !== null) { const blockHtml = blockMatch[1]; // Updated regex to match the URL within the 'url_header' class link const urlMatch = blockHtml.match(/<a[^>]*href=["']([^"']+)["'][^>]*class=["'][^"']*url_header[^"']*["']/i); // Updated regex to match the summary within the 'content' class paragraph const summaryMatch = blockHtml.match(/<p[^>]*class=["'][^"']*content[^"']*["'][^>]*>(.*?)<\/p>/is); const url = urlMatch ? urlMatch[1] : 'No URL found'; const summary = summaryMatch ? summaryMatch[1].replace(/<[^>]*>/g, '').trim() : 'No summary found'; // Remove HTML tags from summary // Add result only if a URL is found (even if summary is not) if (url !== 'No URL found') { resultsArray.push({ url, summary }); } else { log.warn('No URL found in result block', { blockHtml }); } } if (html.length > 50 && resultsArray.length == 0) { log.error(`Got html contents of: \n===========\n${html}\n===========\n but no results parsed.`) } return { content: [{ type: 'text', text: JSON.stringify(resultsArray) }], }; } catch (error) { throw new UserError(`Error fetching "${query}" results from SearXNG ${baseUrl}: ${error instanceof Error ? error.message : String(error)}`); }
  • Helper for standard search mode: performs fetchResults with retry logic across multiple shuffled base URLs if initial attempts fail or return insufficient data.
    async function fetchWithRetry( log: Log, query: string, shuffledUrls: string[], maxRetries: number = 5, time_range?: string, language?: string, ): Promise<ContentResult | undefined> { let response: ContentResult | undefined; let currentUrls = [...shuffledUrls]; try { response = await fetchResults(log, query, currentUrls[0], time_range, language); } catch (error) { log.error('Error during first fetch: ', { error: error instanceof Error ? error.message : String(error) }); } let retries = 0; while (retries < maxRetries && (response === undefined || (!response.content) || (response.content[0] as TextContent)?.text?.length < 10)) { if (retries > 0) { log.error(`Query to ${currentUrls[0]} yielded no data, retrying...`); } await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second before retrying // Try next base URL if available try { if (currentUrls.length > 1) { currentUrls = currentUrls.slice(1); response = await fetchResults(log, query, currentUrls[0], time_range, language); } else { response = await fetchResults(log, query, currentUrls[0], time_range, language); } } catch (error) { log.error('Error fetching results, trying next base URL', { error: error instanceof Error ? error.message : String(error) }); } retries++; } return response; }

Other Tools

Related 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/pwilkin/mcp-searxng-public'

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