Skip to main content
Glama
whw23

searxng-http-mcp

search

Read-onlyIdempotent

Search the web privately using SearXNG metasearch, aggregating results from 200+ engines. Narrow by category or engine, and use pages for comprehensive results.

Instructions

Search the web using SearXNG metasearch engine.

Aggregates results from 200+ search engines (Google, Bing, DuckDuckGo, Brave, etc.) with privacy. Returns results, answers, suggestions, corrections, and infoboxes. Use 'categories' to focus on specific content types. Use 'pages' for more results.

Not suitable for autocomplete suggestions (use autocomplete tool) or discovering available engines/categories (use engine_info tool). Results are cached for 60 seconds.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
queryYesThe search query to use
categoriesNoComma-separated category names to focus on (e.g., 'general,news,science'). Prefer this over 'engines' to narrow results — categories leverage multiple engines automatically. Call engine_info to discover available categories.
enginesNoComma-separated engine names to use (e.g., 'google,arxiv,wikipedia'). Only use when you need a specific source; otherwise prefer 'categories'. Overrides category-based engine selection when set.
languageNoSearch language code (e.g., 'en', 'zh', 'ja', 'de'). Filters results to the specified language. Omit to search all languages.
time_rangeNoRestrict results to those published within this time window. Omit for no time restriction.
safesearchNoSafe search level: 0=off, 1=moderate, 2=strict
pagenoNoStarting page number. Use with 'pages' for pagination.
pagesNoNumber of pages to fetch in parallel (multi-page fanout). Higher values return more results but increase latency. Use 2-3 for comprehensive research.
max_resultsNoMaximum number of results to return. Applied after aggregation across pages.
formatNoResult detail level: 'compact' returns title/url/content only, 'full' includes engines/score/category/date/thumbnailscompact

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
resultYes

Implementation Reference

  • The @mcp.tool decorated async function 'search' that executes the SearXNG web search. It accepts query, categories, engines, language, time_range, safesearch, pageno, pages, max_results, and format parameters, queries the SearXNG backend, caches results, and returns JSON output.
    @mcp.tool(
        annotations=ToolAnnotations(
            readOnlyHint=True,
            destructiveHint=False,
            idempotentHint=True,
            openWorldHint=True,
        )
    )
    async def search(
        query: Annotated[str, Field(
            description="The search query to use",
        )],
        categories: Annotated[str, Field(
            description=(
                "Comma-separated category names to focus on (e.g., 'general,news,science'). "
                "Prefer this over 'engines' to narrow results — categories leverage multiple engines automatically. "
                "Call engine_info to discover available categories."
            ),
        )] = "",
        engines: Annotated[str, Field(
            description=(
                "Comma-separated engine names to use (e.g., 'google,arxiv,wikipedia'). "
                "Only use when you need a specific source; otherwise prefer 'categories'. "
                "Overrides category-based engine selection when set."
            ),
        )] = "",
        language: Annotated[str, Field(
            description=(
                "Search language code (e.g., 'en', 'zh', 'ja', 'de'). "
                "Filters results to the specified language. Omit to search all languages."
            ),
        )] = "",
        time_range: Annotated[
            Literal["day", "week", "month", "year"] | None,
            Field(description="Restrict results to those published within this time window. Omit for no time restriction."),
        ] = None,
        safesearch: Annotated[
            Literal[0, 1, 2],
            Field(description="Safe search level: 0=off, 1=moderate, 2=strict"),
        ] = 0,
        pageno: Annotated[int, Field(
            ge=1,
            description="Starting page number. Use with 'pages' for pagination.",
        )] = 1,
        pages: Annotated[int, Field(
            ge=1, le=5,
            description=(
                "Number of pages to fetch in parallel (multi-page fanout). "
                "Higher values return more results but increase latency. Use 2-3 for comprehensive research."
            ),
        )] = 1,
        max_results: Annotated[int, Field(
            ge=1, le=100,
            description="Maximum number of results to return. Applied after aggregation across pages.",
        )] = 10,
        format: Annotated[
            Literal["compact", "full"],
            Field(description="Result detail level: 'compact' returns title/url/content only, 'full' includes engines/score/category/date/thumbnails"),
        ] = "compact",
    ) -> str:
        """Search the web using SearXNG metasearch engine.
    
        Aggregates results from 200+ search engines (Google, Bing, DuckDuckGo, Brave, etc.)
        with privacy. Returns results, answers, suggestions, corrections, and infoboxes.
        Use 'categories' to focus on specific content types. Use 'pages' for more results.
    
        Not suitable for autocomplete suggestions (use autocomplete tool) or discovering
        available engines/categories (use engine_info tool). Results are cached for 60 seconds.
        """
        fields = COMPACT_FIELDS if format == "compact" else FULL_FIELDS
    
        params: dict = {"q": query, "format": "json"}
        if categories:
            params["categories"] = categories
        if language:
            params["language"] = language
        if time_range is not None:
            params["time_range"] = time_range
        if safesearch:
            params["safesearch"] = str(safesearch)
        if engines:
            params["engines"] = engines
    
        cache_params = {**params, "pageno": pageno, "pages": pages, "format": format}
        cache_k = _cache_key(cache_params)
        cached = _get_cached(cache_k)
        if cached is not None:
            results = cached["results"][:max_results]
            output = {**cached, "results": results, "number_of_results": len(results), "cached": True}
            return json.dumps(output, ensure_ascii=False)
    
        all_results = []
        all_answers: set[str] = set()
        all_suggestions: set[str] = set()
        all_corrections: set[str] = set()
        all_infoboxes = []
        errors: list[str] = []
    
        client = await _get_client()
        tasks = []
        for page in range(pageno, pageno + pages):
            page_params = {**params, "pageno": str(page)}
            tasks.append(
                client.get(
                    f"{SEARXNG_BASE_URL}/search",
                    params=page_params,
                    timeout=30.0,
                )
            )
        responses = await asyncio.gather(*tasks, return_exceptions=True)
    
        for resp in responses:
            if isinstance(resp, Exception):
                errors.append(str(resp))
                continue
            if resp.status_code != 200:
                errors.append(f"HTTP {resp.status_code}")
                continue
            data = resp.json()
            all_results.extend(_trim_result(r, fields) for r in data.get("results", []))
            all_answers.update(data.get("answers", []))
            all_suggestions.update(data.get("suggestions", []))
            all_corrections.update(data.get("corrections", []))
            all_infoboxes.extend(data.get("infoboxes", []))
            for engine_name, error_msg in data.get("unresponsive_engines", []):
                errors.append(f"{engine_name}: {error_msg}")
    
        if not all_results and not all_answers:
            return json.dumps(_build_diagnostics(query, params, errors), ensure_ascii=False)
    
        output: dict = {
            "results": all_results,
            "number_of_results": len(all_results),
        }
        if all_answers:
            output["answers"] = list(all_answers)
        if all_suggestions:
            output["suggestions"] = list(all_suggestions)
        if all_corrections:
            output["corrections"] = list(all_corrections)
        if all_infoboxes:
            output["infoboxes"] = all_infoboxes
    
        _set_cache(cache_k, output)
    
        results = output["results"][:max_results]
        return_data = {**output, "results": results, "number_of_results": len(results)}
        return json.dumps(return_data, ensure_ascii=False)
  • Pydantic Field annotations defining the input schema for the 'search' tool, including query, categories, engines, language, time_range, safesearch, pageno, pages, max_results, and format.
    async def search(
        query: Annotated[str, Field(
            description="The search query to use",
        )],
        categories: Annotated[str, Field(
            description=(
                "Comma-separated category names to focus on (e.g., 'general,news,science'). "
                "Prefer this over 'engines' to narrow results — categories leverage multiple engines automatically. "
                "Call engine_info to discover available categories."
            ),
        )] = "",
        engines: Annotated[str, Field(
            description=(
                "Comma-separated engine names to use (e.g., 'google,arxiv,wikipedia'). "
                "Only use when you need a specific source; otherwise prefer 'categories'. "
                "Overrides category-based engine selection when set."
            ),
        )] = "",
        language: Annotated[str, Field(
            description=(
                "Search language code (e.g., 'en', 'zh', 'ja', 'de'). "
                "Filters results to the specified language. Omit to search all languages."
            ),
        )] = "",
        time_range: Annotated[
            Literal["day", "week", "month", "year"] | None,
            Field(description="Restrict results to those published within this time window. Omit for no time restriction."),
        ] = None,
        safesearch: Annotated[
            Literal[0, 1, 2],
            Field(description="Safe search level: 0=off, 1=moderate, 2=strict"),
        ] = 0,
        pageno: Annotated[int, Field(
            ge=1,
            description="Starting page number. Use with 'pages' for pagination.",
        )] = 1,
        pages: Annotated[int, Field(
            ge=1, le=5,
            description=(
                "Number of pages to fetch in parallel (multi-page fanout). "
                "Higher values return more results but increase latency. Use 2-3 for comprehensive research."
            ),
        )] = 1,
        max_results: Annotated[int, Field(
            ge=1, le=100,
            description="Maximum number of results to return. Applied after aggregation across pages.",
        )] = 10,
        format: Annotated[
            Literal["compact", "full"],
            Field(description="Result detail level: 'compact' returns title/url/content only, 'full' includes engines/score/category/date/thumbnails"),
        ] = "compact",
    ) -> str:
  • The @mcp.tool() decorator registers the 'search' function as an MCP tool with annotations marking it as read-only, non-destructive, idempotent, and open-world.
    @mcp.tool(
        annotations=ToolAnnotations(
            readOnlyHint=True,
            destructiveHint=False,
            idempotentHint=True,
            openWorldHint=True,
        )
    )
  • Helper function '_trim_result' that keeps only specified fields from a search result dict. Used by the search handler to filter results based on format (compact vs full).
    def _trim_result(result: dict, fields: list[str]) -> dict:
        """Keep only specified fields from a search result."""
        return {k: v for k, v in result.items() if k in fields and v}
  • Helper function '_build_diagnostics' that builds diagnostic info with suggestions when search returns zero results.
    def _build_diagnostics(query: str, params: dict, errors: list[str]) -> dict:
        """Build diagnostic info when search returns zero results."""
        tips = [
            "Try broader or different keywords",
            "Remove time_range filter if set",
            "Try different categories or engines",
        ]
        if params.get("language"):
            tips.append(f"Try removing language filter (currently: {params['language']})")
        if params.get("engines"):
            tips.append("Some specified engines may be unresponsive — try without engines filter")
        if params.get("time_range"):
            tips.append(f"Time range '{params['time_range']}' may be too restrictive")
    
        diag: dict = {
            "query": query,
            "message": "No results found",
            "suggestions": tips,
        }
        if errors:
            diag["errors"] = errors
        return diag
Behavior5/5

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

Annotations already indicate readOnly, idempotent, and open world. Description adds that results are cached for 60 seconds, which is valuable behavioral context beyond annotations.

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

Conciseness4/5

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

Description is well-structured with clear front-loading of purpose. Though three paragraphs, each sentence adds value; could be slightly tighter but no significant waste.

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

Completeness5/5

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

Despite 10 parameters, description covers all key variants (time range, safesearch, pagination, format) and mentions return types. Output schema handles return values, so description is complete for agent use.

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?

With 100% schema coverage, baseline is 3. Description adds value by explaining when to use 'categories' over 'engines' and detailing 'format' options, but mostly reinforces schema.

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?

Description clearly states tool is for web search using SearXNG, aggregating from 200+ engines with privacy. It explicitly distinguishes itself from siblings (autocomplete and engine_info) by mentioning what it's not suitable for.

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

Usage Guidelines5/5

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

Provides explicit guidance on when to use this tool (web search), when not to (autocomplete, engine discovery), and how to use parameters like 'categories' vs 'engines' and 'pages' for pagination.

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/whw23/searxng_http_mcp'

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