kleinanzeigen-mcp
by jnslmk
README.md
# kleinanzeigen-mcp
An [MCP](https://modelcontextprotocol.io) server that lets an LLM search
[Kleinanzeigen.de](https://www.kleinanzeigen.de), Germany's largest classifieds
site. Built for self-hosting: one container, no API keys, no per-query credits.
Scraping is done by [DanielWTE/ebay-kleinanzeigen-api][upstream], which is baked
into the image at a pinned commit. This project is the MCP layer on top.
[upstream]: https://github.com/DanielWTE/ebay-kleinanzeigen-api
## Tools
| Tool | What it does |
|------|--------------|
| `search_listings` | Search by keyword, location, radius and price range. Returns listing summaries. |
| `get_listing` | Full detail page for one listing id. |
| `get_listings_batch` | Full details for several ids at once — the normal follow-up to a search. |
| `search_by_url` | Search from a pasted Kleinanzeigen URL, preserving filters that `search_listings` cannot express (vehicle make, model year, room count, …). |
| `parse_search_url` | Explain which filters a URL encodes. Makes no network request. |
The intended flow is `search_listings` → pick interesting ids →
`get_listings_batch`. Search results deliberately omit descriptions so a broad
search does not blow up the model's context window.
## Running it
```bash
docker run --rm -p 8000:8000 ghcr.io/jnslmk/kleinanzeigen-mcp:latest
```
The server speaks streamable HTTP at `http://localhost:8000/mcp`, with a plain
`GET /healthz` for container healthchecks. Set `MCP_TRANSPORT=stdio` to run it
as a local stdio server instead.
Chromium needs room to work — give the container at least 1.5 GB of memory.
### Configuration
| Variable | Default | Purpose |
|----------|---------|---------|
| `MCP_TRANSPORT` | `http` | `http` (streamable HTTP) or `stdio` |
| `MCP_HOST` | `0.0.0.0` | Bind address |
| `MCP_PORT` | `8000` | Bind port |
| `MCP_PATH` | `/mcp` | MCP endpoint path |
| `KZ_MAX_CONTEXTS` | `4` | Chromium browser contexts to pool |
| `KZ_MAX_CONCURRENT` | `2` | Concurrent scrapes |
| `KZ_MAX_PAGE_COUNT` | `5` | Cap on pages per search (~25 listings each) |
| `KZ_MAX_BATCH_SIZE` | `20` | Cap on ids per `get_listings_batch` call |
| `LOG_LEVEL` | `INFO` | Python log level |
The `KZ_MAX_*` defaults are tuned for a chat agent making one request at a time.
Raise them for throughput, at the cost of memory and a greater chance of
tripping Kleinanzeigen's bot detection.
### LibreChat
```yaml
mcpServers:
kleinanzeigen:
type: streamable-http
url: "http://kleinanzeigen-mcp:8000/mcp"
timeout: 120000
chatMenu: true
mcpSettings:
allowedAddresses:
- "kleinanzeigen-mcp:8000"
```
`allowedAddresses` is required: LibreChat's SSRF guard blocks MCP URLs that
resolve to private addresses, which a sibling container always does. The
timeout is generous on purpose — a cold search has to start a browser context
and load real pages.
### Claude Code
```bash
claude mcp add --transport http kleinanzeigen http://localhost:8000/mcp
```
## Development
```bash
uv venv && uv pip install -e .
# The scrapers are not on PyPI, so point PYTHONPATH at a checkout of upstream:
git clone https://github.com/DanielWTE/ebay-kleinanzeigen-api.git /tmp/kz-api
pip install -r /tmp/kz-api/requirements.txt && playwright install chromium
PYTHONPATH=/tmp/kz-api python -m kleinanzeigen_mcp
```
### Upgrading the scraper
`UPSTREAM_SHA` in the `Dockerfile` pins the upstream commit. It is pinned rather
than tracking `main` because this server imports upstream's scraper functions
directly — `ultra_optimized_scrape_inserate`, `get_inserate_details_optimized`
and `scrape_by_url` — instead of going through its HTTP API. That avoids a
second process and an HTTP hop, but it means an upstream refactor can break this
image at runtime rather than at build time. Bump the SHA, rebuild, smoke-test
each tool, then ship.
`patches/astro-results-fields.patch` rides on top of the pin: Kleinanzeigen's
Astro relaunch (Sept 2026) dropped the classic card markup, and upstream's own
fix restored adid/url/title/location but not price, description or date — the
patch adds card-content fallbacks for those three. The Dockerfile applies it
with `git apply --check`, so when the pin moves past code the patch no longer
fits, the build fails loudly instead of silently shipping a regression.
Upstream absorbing this fix is the exit condition for the patch — drop it the
moment a newer pin makes it redundant.
## Images
`ghcr.io/jnslmk/kleinanzeigen-mcp` — multi-arch (`linux/amd64`, `linux/arm64`),
built by GitHub Actions on native runners for each architecture.
| Tag | Meaning |
|-----|---------|
| `latest` | Newest build of `main` |
| `sha-<full-sha>` | A specific commit |
| `v1.2.3`, `v1.2` | Release tags |
## Caveats
Kleinanzeigen has no public API, so this scrapes the site with a headless
browser. That means it can break whenever they change their markup, and heavy
or parallel use may trip bot detection — particularly from a datacenter IP.
Scraping is also at odds with Kleinanzeigen's terms of service. Keep it to
personal-scale use.
## License
MIT. Bundles [DanielWTE/ebay-kleinanzeigen-api][upstream], also MIT — see
[LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues