Skip to main content
Glama

search_ads

Search Leboncoin classified ads using filters for category, region, price, and sort order. Retrieve up to 10 results per query, limited to 10 requests per hour.

Instructions

Recherche des annonces Leboncoin avec filtres. Limité à 10 requêtes/heure (DataDome).

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
textNo
categoryNo
regionNo
sortNoNEWEST
limitNo
pageNo
price_minNo
price_maxNo

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
resultYes

Implementation Reference

  • The @mcp.tool() decorator registers 'search_ads' as an MCP tool on the FastMCP 'leboncoin' server instance.
    @mcp.tool()
  • The search_ads function (decorated with @mcp.tool()) implements the core logic: accepts optional filters (text, category, region, sort, limit, page, price_min, price_max), resolves enums from the lbc library, enforces rate limiting via _check_rate_limit(), calls client.search() with the constructed kwargs, and returns a JSON string with results and metadata.
    def search_ads(
        text: Annotated[str, "Mots-clés de recherche (ex: 'vélo', 'iPhone 14')"] = "",
        category: Annotated[
            str,
            "Catégorie (ex: 'LOISIRS_VELOS', 'VEHICULES_VOITURES', 'IMMOBILIER_VENTES_IMMOBILIERES'). "
            "Valeur vide = toutes catégories.",
        ] = "",
        region: Annotated[
            str,
            "Région (ex: 'ILE_DE_FRANCE', 'AUVERGNE_RHONE_ALPES', 'BRETAGNE'). Vide = France entière.",
        ] = "",
        sort: Annotated[
            Literal["NEWEST", "OLDEST", "CHEAPEST", "EXPENSIVE", "RELEVANCE"],
            "Tri des résultats.",
        ] = "NEWEST",
        limit: Annotated[int, "Nombre max d'annonces à retourner (1-35)."] = 10,
        page: Annotated[int, "Numéro de page (commence à 1)."] = 1,
        price_min: Annotated[int, "Prix minimum en euros (0 = pas de minimum)."] = 0,
        price_max: Annotated[int, "Prix maximum en euros (0 = pas de maximum)."] = 0,
    ) -> str:
        """Recherche des annonces Leboncoin avec filtres. Limité à 10 requêtes/heure (DataDome)."""
        if err := _check_rate_limit():
            return err
        client = _get_client()
    
        kwargs: dict = {
            "sort": Sort[sort],
            "limit": min(max(limit, 1), 35),
            "page": page,
        }
    
        if text:
            kwargs["text"] = text
    
        if category:
            try:
                kwargs["category"] = Category[category]
            except KeyError:
                valid = [e.name for e in Category][:20]
                return json.dumps({"error": f"Catégorie '{category}' inconnue. Exemples: {valid}"})
    
        if region:
            try:
                kwargs["locations"] = Region[region]
            except KeyError:
                valid = [e.name for e in Region][:10]
                return json.dumps({"error": f"Région '{region}' inconnue. Exemples: {valid}"})
    
        if price_min > 0 or price_max > 0:
            pmin = price_min if price_min > 0 else 0
            pmax = price_max if price_max > 0 else 0
            if pmax > 0:
                kwargs["price"] = (pmin, pmax)
            else:
                kwargs["price"] = (pmin, 9_999_999)
    
        try:
            results = client.search(**kwargs)
        except DatadomeError as e:
            return json.dumps({"error": f"DataDome: {e}. Utilisez un réseau résidentiel."})
    
        ads = [_ad_to_dict(ad) for ad in results.ads]
        return json.dumps({
            "total": results.total,
            "max_pages": results.max_pages,
            "page": page,
            "count": len(ads),
            "ads": ads,
        }, ensure_ascii=False)
  • Input schema defined via type annotations on search_ads parameters: text (str), category (str), region (str), sort (Literal['NEWEST','OLDEST','CHEAPEST','EXPENSIVE','RELEVANCE']), limit (int, 1-35), page (int), price_min (int), price_max (int). All have Annotated descriptions used by FastMCP for schema generation.
    def search_ads(
        text: Annotated[str, "Mots-clés de recherche (ex: 'vélo', 'iPhone 14')"] = "",
        category: Annotated[
            str,
            "Catégorie (ex: 'LOISIRS_VELOS', 'VEHICULES_VOITURES', 'IMMOBILIER_VENTES_IMMOBILIERES'). "
            "Valeur vide = toutes catégories.",
        ] = "",
        region: Annotated[
            str,
            "Région (ex: 'ILE_DE_FRANCE', 'AUVERGNE_RHONE_ALPES', 'BRETAGNE'). Vide = France entière.",
        ] = "",
        sort: Annotated[
            Literal["NEWEST", "OLDEST", "CHEAPEST", "EXPENSIVE", "RELEVANCE"],
            "Tri des résultats.",
        ] = "NEWEST",
        limit: Annotated[int, "Nombre max d'annonces à retourner (1-35)."] = 10,
        page: Annotated[int, "Numéro de page (commence à 1)."] = 1,
        price_min: Annotated[int, "Prix minimum en euros (0 = pas de minimum)."] = 0,
        price_max: Annotated[int, "Prix maximum en euros (0 = pas de maximum)."] = 0,
    ) -> str:
  • Helper function _check_rate_limit() enforces a sliding window rate limit (max 10 searches per hour) to avoid DataDome flags. Returns a JSON error string if limit exceeded, otherwise None.
    def _check_rate_limit() -> str | None:
        """Retourne un message d'erreur JSON si la limite est atteinte, sinon None."""
        now = time.monotonic()
        while _search_timestamps and now - _search_timestamps[0] > _SEARCH_WINDOW:
            _search_timestamps.popleft()
        if len(_search_timestamps) >= _SEARCH_LIMIT:
            oldest = _search_timestamps[0]
            wait = int(_SEARCH_WINDOW - (now - oldest)) + 1
            return json.dumps({
                "error": f"Rate limit atteint ({_SEARCH_LIMIT} recherches/heure). "
                         f"Réessayez dans {wait // 60} min {wait % 60} s."
            })
        _search_timestamps.append(now)
        return None
  • Helper function _ad_to_dict() converts an lbc ad object into a JSON-serializable dictionary with id, subject, price, url, city, zipcode, department, region, category, first_publication_date, has_phone, and images_count.
    def _ad_to_dict(ad) -> dict:
        return {
            "id": ad.id,
            "subject": ad.subject,
            "price": ad.price,
            "url": ad.url,
            "city": ad.location.city,
            "zipcode": ad.location.zipcode,
            "department": ad.location.department_name,
            "region": ad.location.region_name,
            "category": ad.category_name,
            "first_publication_date": str(ad.first_publication_date) if ad.first_publication_date else None,
            "has_phone": ad.has_phone,
            "images_count": len(ad.images) if ad.images else 0,
        }
Behavior3/5

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

The rate limit is disclosed, but no other behavioral traits like authentication needs, data freshness, or handling of missing results are mentioned. With no annotations, the description carries full burden and only covers one aspect.

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 extremely concise with two sentences, front-loading the purpose and rate limit constraint.

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?

Given 8 parameters, an output schema, and a sibling tool, the description is insufficient. It lacks parameter guidance, pagination logic, and return value explanation, despite the output schema existing to mitigate completeness.

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

Parameters1/5

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

Schema coverage is 0%, and the description only says 'with filters' without explaining any of the 8 parameters (text, category, region, etc.), leaving the agent without meaning for the inputs.

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 it searches Leboncoin ads with filters, distinguishing it from sibling tool get_ad_detail which retrieves individual ad details.

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 mentions a rate limit (10 requests/hour) but provides no guidance on when to use this tool versus the sibling, nor any prerequisites or limitations on filters.

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/TheoGirardin/mcp-lbc'

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