Skip to main content
Glama
Samxel

willhaben-mcp

by Samxel

An MCP server that lets an AI search willhaben.at and pull the full details of ads. It covers the marketplace (Marktplatz), the Auto & Motor cars vertical and Immobilien (real estate), wrapping willhaben's reverse-engineered mobile-app API and returning the important fields to the AI.

Highlights

  • Visual Listing Analysis
    Downloads and analyzes the actual ad photos, allowing vision-capable AI to inspect an item's condition, wear, damage, completeness, and other visual details instead of relying only on the seller's description.

  • Full access to willhaben filters
    Search with the complete range of available filters across Marketplace, Immobilien and Auto & Motor including categories, brands, condition, size, color, price, region, vehicle specs, equipment, and much more.

  • From discovery to full inspection
    Find relevant ads, then retrieve complete listing details, full descriptions, attributes, precise locations, and all available photos for a deeper analysis.

Related MCP server: Secondhand MCP

Setup

pip install -r requirements.txt

Run

python main.py

The server starts over streamable HTTP and prints where it's listening:

Starting willhaben-mcp server on http://127.0.0.1:8000/mcp

Point your MCP client at that URL. Host, port and path live at the top of main.py.

Add it to Claude (and other MCP clients)

The server speaks streamable HTTP, so most clients only need the URL it printed on startup.

Claude Code

One command, no config file:

claude mcp add --transport http willhaben http://127.0.0.1:8000/mcp

Add --scope user to have it in every project instead of only the current one. claude mcp list shows whether the connection came up, claude mcp remove willhaben takes it out again.

Cursor, Codex, VS Code and other JSON-config clients

Same URL, in the client's MCP config:

{
  "mcpServers": {
    "willhaben": {
      "type": "http",
      "url": "http://127.0.0.1:8000/mcp"
    }
  }
}

Claude Desktop

Custom connectors (Settings > Connectors) are dialled from Anthropic's cloud, so they cannot reach a server bound to your own machine. Two ways around it:

Bridge over stdio with mcp-remote (needs Node.js). Open Settings > Developer > Edit Config, add the willhaben entry to claude_desktop_config.json and restart Claude Desktop:

{
  "mcpServers": {
    "willhaben": {
      "command": "cmd",
      "args": ["/c", "npx", "-y", "mcp-remote", "http://127.0.0.1:8000/mcp"]
    }
  }
}

On macOS/Linux drop the Windows wrapper: "command": "npx" with "args": ["-y", "mcp-remote", "http://127.0.0.1:8000/mcp"].

Or expose the server (cloudflared, ngrok, or run it on a box with a public hostname) and add that HTTPS URL under Settings > Connectors > Add custom connector. That route also makes the tools available in claude.ai and the mobile apps, not just the desktop client but the API it talks to is then reachable by whoever finds the URL, so put auth in front of it.

To stop confirming every call, open Settings > Connectors > willhaben and set the tools' dropdown on the right to Always allow.

Tools

search_willhaben(keyword, ...)

Search the marketplace. keyword is optional if you pass a category.

Filters:

  • category (id or name)

  • condition (new, used, refurbished, ...)

  • clothing_size and shoe_size

  • color, pattern

  • brand

  • region, seller type, price range, PayLivery, last 48h

  • title_only (every keyword word must be in the title, not just anywhere in the ad text) and exclude (drop titles carrying any of these words, e.g. ["pro", "max", "mini"] to keep a base model clean of its own trim levels) — both applied here, paging on until rows matches are found or 250 ads have been scanned. exclude matches a whole word or a German compound tail ("kabel" catches "Ladekabel") but never a mere prefix, so "pro" spares every "Prozessor"

  • require — terms that must appear anywhere in the title or description, with a number and its unit matched however it is spelled: ["128gb"], ["128 GB"] and ["128 gb"] are one filter and each finds all three spellings. This is where specs belong. Putting them in keyword instead is the one mistake that quietly breaks a search — sellers write the same storage size nine different ways and one ad in five leaves it out of the title entirely, so keyword="iPhone 13 128 GB", title_only=True finds almost nothing while keyword="iPhone 13", require=["128gb"] finds what you meant

  • accessories are what none of these fix, because a case carries the phone's exact name too. Excluding nouns like "case" or "akku" backfires — those appear in real listings' titles ("Akku 100%") — so use a price_from floor at ~15–20% of the product's real price instead (Geizhals' get_model_price_range gives you that number)

  • hide_reserved

  • handover ("versand" / "abholung") — shipping is not in willhaben's search response at all, so this costs one detail request per surviving candidate, capped at 40 and reported as detail_lookups

  • sorting and pagination

Returns a trimmed list of hits, each with a numeric price_amount, a status ("active" / "reserved" / "sold") and a reserved flag read out of the title — willhaben itself always reports an ad as active. next_offset and has_more say where to continue and when the catalogue is exhausted (next_offset is null once it is).

list_categories(query, parent_id)

Find category ids for the category filter.

  • query: search the whole tree by name

  • parent_id: browse one level down (omit both for the top-level categories)

The full tree (~3500 categories) ships with the server in data/marktplatz/categories.json.

search_brands(category, term)

Brands are category-specific and there are 1000+ per category, so this is a type-ahead. Pass a category and a search term to get matching brand ids, then hand an id to search_willhaben(brand=...).

Note: condition, sizes, color, pattern and brand are category-dependent. Applying them in a broad category can return zero hits, so drill into a specific subcategory first.

search_autos(make, model, ...)

Search used cars (Gebrauchtwagen). All filters are optional.

  • make (id or name) and model (id or name, e.g. "3er-Reihe")

  • car_type, fuel, transmission, wheel_drive

  • condition (Gebrauchtwagen, Neuwagen, Oldtimer, ...), color, dealer

  • equipment (e.g. Sitzheizung, Anhängerkupplung)

  • ranges: price_from/to, year_from/to, mileage_from/to, power_from/to (kW)

  • warranty, condition_report (Pickerl §57a), region, last 48h, sorting, paging

Enumerated filters accept the willhaben label or id, and an inverted range is rejected instead of silently ignored. Results include the car fields (make, model, year, mileage, fuel, transmission) and give power as both power_kw and power_ps, since willhaben stores kW while ads and buyers talk in PS. The filter and make data ships in data/auto-motor/filters.json.

list_car_makes(query)

List car make ids for search_autos. Optional query filters by name.

list_car_models(make)

List the models of a make (fetched live, since models are make-specific).

search_immobilien(property_type, ...)

Search real estate. Pick a property_type first (buy vs rent and the kind of property); everything else is optional.

  • property_type: id or name, e.g. "Wohnung mieten", "Haus kaufen", "Grundstücke", "Gewerbeimmobilie mieten", or "Alle Immobilien" (default)

  • object_type (e.g. "Einfamilienhaus", "Dachgeschosswohnung"), rooms ("1"-"5", "6-9", "10+")

  • features (Garage, Keller, Einbauküche, ...), outdoor (Balkon, Terrasse, Garten, ...)

  • ranges: price_from/to, area_from/to (living m²), plot_from/to (plot m²)

  • region, last 48h, sorting, paging

Filters that don't apply to the chosen type are ignored by willhaben; an inverted range is rejected here rather than silently dropped. Results carry the numeric price_amount and price_per_m2 next to living_area_m2, plot_area_m2, rooms, floor, district and address. The type and filter data ships in data/immobilien/filters.json.

list_immobilien_types()

List the real-estate property types for search_immobilien.

get_ad_detail(ad_id)

Everything about one ad: the full description, all images, itemised attributes, category path and precise location. Works across verticals. Run it on an id you got from a search. The mobile API caps the description near 600 characters, so when it looks cut off the full clean text is pulled from the ad's web page.

This is also the only place willhaben states delivery: handover ("Selbstabholung", "Versand") with ships / pickup_only alongside it — PayLivery in the search is a payment method and says nothing about shipping.

get_ad_images(ad_id, max_images=4)

Download an ad's photos server-side and return them as real image content (base64), so a vision-capable client sees the pictures instead of just URLs. Handy when you want to actually look at a listing.

get_ad_seller(ad_id)

Who is selling: name, private vs dealer, rating and reply time (private sellers), the member-since / created date, and location. Useful for a trust or plausibility check the ad itself does not answer.

Notes

  • The detail API only speaks HTTP/2 and needs an x-wh-application-token. That token is issued by willhaben (valid 30 days) in exchange for a signed request, so main.py fetches a live token on demand and refreshes it automatically on a 401 -- no token to hand-edit. Only if willhaben rotates the signing key does the stored WH_TOKEN_REQUEST stop working; the comment above it in main.py explains how to capture a fresh one from the app.

  • Generated data lives under data/, one folder per vertical: data/marktplatz/categories.json (the ~3500-category tree), data/auto-motor/filters.json (car filters, options and makes) and data/immobilien/filters.json (property types, filters and options). main.py loads them at startup. Re-crawl them if willhaben changes.

  • API details are documented in search_api.md.

  • This uses willhaben's internal API, not an official one. Be nice to it.

Disclaimer

This is an independent, unofficial project and is not affiliated with, endorsed by, or connected to willhaben. "willhaben" and all related trademarks belong to their respective owners.

It's published for educational and research purposes only. It talks to willhaben's internal API, which is not meant for public use and may break or change at any time. You are responsible for how you use it: respect willhaben's Terms of Service, robots rules and applicable law, and don't hammer their servers or process personal data from listings. No warranty of any kind; use at your own risk.

If you're a rights holder and have a concern about this repository, please open an issue and I'll respond promptly.

License

MIT LICENSE. Provided "as is", without warranty.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    F
    maintenance
    Enables AI agents to search and retrieve listings from Sweden's largest second-hand marketplaces, Blocket and Tradera. Returns unified data including prices, images, seller information, and direct links to listings.
    8
    -
  • A
    license
    A
    quality
    D
    maintenance
    Enables searching Austria's largest classifieds marketplace (willhaben.at) for real estate, cars, jobs, and second-hand items via natural-language queries and structured filters, with full listing details.
    2
    7
    10 npm
    2
    MIT
  • A
    license
    Not graded
    quality
    F
    maintenance
    Enables AI assistants to search and view advertisements on Marktplaats.nl with extensive filtering options.
    16
    MIT