bolha
by vsterle20
README.md
# Bolha
[](https://github.com/vsterle20/bolha.com-mcp/actions/workflows/ci.yml)
[](https://www.npmjs.com/package/bolha)
[](LICENSE)
[](https://nodejs.org)
A client for **[bolha.com](https://www.bolha.com)**, Slovenia's largest classifieds marketplace —
as a reusable **library**, a **CLI**, and an **MCP server**, so that humans and AI agents can use
Bolha reliably without the web UI.
```text
Bolha website → bolha core client → ┌────────────┐
(HTML/JSON) (src/core) │ CLI + MCP │
└────────────┘
```
One core library, two front-ends. No duplicated scraping logic.
> **Unofficial.** Not affiliated with or endorsed by Styria digital marketplaces d.o.o.,
> the operator of bolha.com. Read-only by design: it never writes to the site.
> **Publishing this?** See [PREPARE.md](PREPARE.md) — three things need your input first.
---
## Why this exists
Bolha has **no public API**. `api.bolha.com` 301-redirects to the homepage, and there is no
Bolha MCP server (both verified — see [`research/EXISTING_PROJECTS.md`](research/EXISTING_PROJECTS.md)).
The site is nonetheless fully machine-readable: every page embeds a complete JSON payload in
`window.__INITIAL_STATE__`. This project reads **that**, rather than scraping CSS selectors,
which makes it both simpler and far more resistant to layout changes.
> **Research-first.** Nothing here was written from memory of the site. Every URL, parameter,
> id and field was verified against the live site — see [`AGENTS.md`](AGENTS.md) §3 and the
> [`research/`](research/) knowledge base.
---
## Features
| Area | Supported |
|---|---|
| **Search** | Text search, category restriction, price range, condition, image-only, online-payment, sort, pagination |
| **Categories** | Top-level tree, category detail with subcategories, category-specific filters, listings counts |
| **Filters** | Discovered dynamically from the site, so vehicle/real-estate filters work without special-casing |
| **Locations** | 238 distinct locality slugs from Bolha's public sitemap: 11 Slovenian regions + 184 towns/settlements |
| **Region filter** | Works in **every** category — exact locality path in real estate, measured radius everywhere else |
| **Facets** | Category facet pages (e.g. all 60 vehicle makes under `/avto-oglasi`) are listed and browsable |
| **Listings** | Full detail: description, all image sizes, seller, category path, Bolha's attribute table, schema.org data |
| **Sorting** | `relevance` (search only), `new`, `old`, `cheap`, `expensive`, `distance` |
| **Pagination** | Multi-page walking, de-duplicated, bounded for safety |
| **Sellers** | Private sellers (`/uporabnik/`) and business stores (`/trgovina/`) with their current listings |
| **Diagnostics** | `bolha doctor` — nine live checks so silent breakage is visible |
| **MCP** | Six tools over stdio with validated structured output |
**Not supported (deliberately):** posting, editing, deleting, favouriting, messaging, or any
account change. Those require authentication and are intentionally out of scope — see
[Authentication](research/Authentication.md).
---
## Requirements
- **Node.js ≥ 20** (the MCP SDK requires it)
## Installation
```bash
git clone <this-repo> bolha
cd bolha
npm install
npm run build
```
Run the CLI directly, or link it onto your `PATH`:
```bash
node dist/cli/index.js --help
# or, globally:
npm link
bolha --help
```
---
## CLI usage
```bash
bolha search <query> # search listings
bolha browse <category> # browse a category by slug or URL
bolha listing <url> # full detail for one listing
bolha categories # top-level categories
bolha category <slug> # category detail + its filters
bolha facets <category> # facet sub-pages (makes, brands, …)
bolha locations [category] # localities: regions and towns
bolha regions [category] # the eleven Slovenian regions
bolha towns [category] # towns and settlements
bolha location-categories # which categories support locality filtering
bolha filters [category] # filters the site advertises
bolha sort [category] # sort orders available
bolha seller <slug|url> # a private seller or business store
bolha doctor # live diagnostics
```
### Examples
```bash
# Plain search
bolha search "RTX 5090"
# Machine-readable output — nothing but JSON on stdout
bolha search "iPhone" --json | jq '.listings[] | {id, title, price: .price.formatted}'
# Filtered + sorted
bolha search "iPhone" --condition used --min-price 100 --max-price 900 --sort cheap
# Restrict to a category
bolha search "kolo" --category rekreacija-sport --limit 50
# Browse a category by price
bolha browse avto-oglasi --min-price 5000 --max-price 20000 --sort cheap
# Radius search around a city
bolha browse nepremicnine --lat 46.0511 --lng 14.5051 --radius 20
# Category-specific filter (vehicles) via the escape hatch
bolha browse avto-oglasi --param "yearManufactured[min]=2020" --param "condition[used]=1"
# Narrow a category by make: /avto-oglasi links every make as a facet page
bolha facets avto-oglasi
bolha browse avto-oglasi/audi --sort cheap
# Filter by place. Localities are ordinary category path segments.
bolha locations prodaja-hise --query maribor
bolha browse prodaja-hise/maribor --sort cheap
# Region filter — works in any category, not just property.
bolha regions # the 11 Slovenian regions
bolha towns prodaja-hise # 184 towns and settlements
bolha browse avto-oglasi --region koroska # vehicles in Koroška
bolha browse racunalnistvo --region gorenjska # computers in Gorenjska
bolha search "hiša" --region koroska --category prodaja-hise
# Inspect one listing (copy the URL from any search)
bolha listing /apple-iphone/prodam-apple-iphone-14-oglas-16445305
# Who is this seller?
bolha seller _borut_007
bolha seller https://www.bolha.com/trgovina/primer-trgovina
# Check everything still works
bolha doctor --verbose
```
### Options
| Option | Meaning |
|---|---|
| `--limit <n>` | Max listings (default 25, cap 500) |
| `--page <n>` | 1-based page (max 100) |
| `--sort <order>` | `relevance \| new \| old \| cheap \| expensive \| distance` |
| `--category <slug>` / `--category-id <n>` | Restrict to a category |
| `--min-price` / `--max-price` | Price range in EUR |
| `--condition <c>` | Repeatable: `new \| used \| defective` |
| `--lat` `--lng` `--radius` | Radius search |
| `--location <id>` | Repeatable Bolha location id |
| `--ads-with-images` | Only listings with a photo |
| `--param <k=v>` | Any other Bolha URL parameter (repeatable) |
| `--json` | Pure JSON on stdout |
| `--verbose` / `-v` | Log every HTTP request to stderr |
| `--timeout` `--concurrency` `--retries` | Network tuning |
| `--no-cache` / `--cache-dir` | Cache control |
| `--help`, `--version` | |
### Exit codes
| Code | Meaning |
|---|---|
| `0` | Success |
| `2` | Usage error (bad argument, limit above cap) |
| `3` | Not found / no results / removed listing / parent-category page |
| `4` | Network, HTTP or parse failure |
| `130` | Interrupted |
---
## Library
```ts
import { BolhaClient } from 'bolha';
const bolha = new BolhaClient();
const page = await bolha.search({ query: 'iPhone', maxPrice: 900, limit: 10 });
console.log(page.totalCount, page.listings[0]?.title);
const listing = await bolha.getListing(page.listings[0]!.url);
console.log(listing.price.formatted, listing.images.length, listing.attributes);
```
Main methods:
| Method | Returns |
|---|---|
| `search(options)` | `ListingPage` |
| `browseCategory(slug, options)` | `ListingPage` |
| `getListing(urlOrPath)` | `ListingDetail` |
| `getTopCategories()` | `Category[]` |
| `getCategory(slug)` | `CategoryDetail` |
| `getFilters({ categorySlug })` | `FilterDefinition[]` |
| `getSortOrders({ categorySlug })` | `SortOption[]` |
| `getSeller(ref, options)` | `SellerProfile` |
| `resolveSellerFromListing(url)` | `string \| null` |
Every method returns typed, structured data — never raw HTML. Errors are `BolhaError`s with a
stable `code` (`NOT_FOUND`, `PARENT_CATEGORY`, `SITE_CHANGED`, `LIMIT_EXCEEDED`, …).
---
## MCP server
Six tools, each with a strict input schema and validated structured output:
| Tool | Purpose |
|---|---|
| `bolha_search` | Text search with filters and sorting |
| `bolha_browse_category` | Listings in one category, no query needed |
| `bolha_get_categories` | Discover category slugs |
| `bolha_get_category` | One category's subcategories, filters and sort orders |
| `bolha_get_facets` | A category's facet sub-pages (vehicle makes, brand tags) |
| `bolha_get_locations` | Slovenian localities (regions and towns) for a category or free-text search |
| `bolha_get_listing` | Full detail of one listing |
| `bolha_get_regions` | The eleven Slovenian statistical regions |
| `bolha_get_location_categories` | Which categories support locality filtering |
| `bolha_search_in_location` | Text search scoped to a region or town |
| `bolha_get_seller` | Seller or store profile |
### Configuration
**Codex / Claude Desktop / any stdio MCP client:**
```json
{
"mcpServers": {
"bolha": {
"command": "node",
"args": ["/absolute/path/to/bolha/dist/mcp/server.js"]
}
}
}
```
**Environment variables** (all optional):
| Variable | Effect |
|---|---|
| `BOLHA_CACHE_DIR` | Cache location (default `~/.bolha/cache`) |
| `BOLHA_NO_CACHE=1` | Disable the cache |
| `BOLHA_USER_AGENT` | Override the `User-Agent` |
| `BOLHA_VERBOSE=1` | Log requests to **stderr** |
### Context discipline
Search results are **compact summaries** — id, title, price, location, date, seller, URL — with
no descriptions. An agent can choose which listings matter, then call `bolha_get_listing` for the
full text. Listings default to at most 100 per call and descriptions are truncated at 4000
characters, so a search cannot flood a model's context.
The tools speak human terms (`new`/`used`/`defective`, `cheap`/`new`), never Bolha's internal
numeric codes.
---
## Configuration
No configuration is required. Optional environment variables:
| Variable | Default | Purpose |
|---|---|---|
| `BOLHA_USER_AGENT` | descriptive default | Identify your client to Bolha |
| `BOLHA_CACHE_DIR` | `~/.bolha/cache` | Cache location |
| `BOLHA_NO_CACHE` | unset | Disable caching |
| `BOLHA_VERBOSE` | unset | Request logging to stderr |
The cache stores only public page data, is keyed by URL, honours per-kind TTLs (categories 6 h,
searches 1 min) and is invalidated automatically when the library's output shape changes. Session
state and credentials live under `~/.bolha/` and are **never** written to the repository.
---
---
## Setup: Claude Code and Claude Desktop
The MCP server is `dist/mcp/server.js`. It speaks stdio, needs **no API key**, and
reads only public bolha.com pages.
Build it once first (it is already built in this checkout, but you need it after
any `git pull` or code change):
```bash
cd D:\marketplace\bolha
npm install
npm run build
```
Quick check that it works before wiring it into anything:
```bash
npm run doctor
```
### Claude Code (terminal agent)
Run this once:
```bash
claude mcp add bolha --scope user -- node D:\marketplace\bolha\dist\mcp\server.js
```
Then verify:
```bash
claude mcp list # should show: bolha: node D:\marketplace\bolha\dist\mcp\server.js - √ Connected
```
Or add it interactively from inside a `claude` session with `/mcp`.
**Scopes**
| Scope | Flag | Where it is stored |
|---|---|---|
| user (all your projects) | `--scope user` | `~/.claude.json` |
| project (this repo only) | `--scope project` | `.mcp.json` in the repo |
| local (this one project, you only) | `--scope local` | `~/.claude.json`, project-scoped |
Prefer `user` so it follows you across directories. Use `project` if you want the
repo to declare the dependency for your whole team — but then commit `.mcp.json`.
**Windows note.** If `claude` is not on your PATH in the terminal Claude Code runs
from, replace `node` with the absolute interpreter path:
```bash
claude mcp add bolha --scope user -- "C:\Program Files\nodejs\node.exe" D:\marketplace\bolha\dist\mcp\server.js
```
### Claude Desktop (GUI app)
Desktop only reads MCP servers from its own JSON file — the `claude mcp add`
command above does **not** configure it.
Edit:
```
%APPDATA%\Claude\claude_desktop_config.json
```
(on your machine: `C:\Users\vilis\AppData\Roaming\Claude\claude_desktop_config.json`)
Add a `bolha` entry alongside the servers already there, keeping the existing
ones intact:
```json
{
"mcpServers": {
"davinci-resolve": { "...": "keep what is already there" },
"FaceMCP": { "...": "keep what is already there" },
"secondhand-facebook": { "...": "keep what is already there" },
"bolha": {
"command": "node",
"args": ["D:\\marketplace\\bolha\\dist\\mcp\\server.js"]
}
}
}
```
Then **fully quit and restart Claude Desktop** (close the tray icon too — closing
the window is not enough on Windows). The server appears in the tools menu
(🔨 icon) as `bolha_search`, `bolha_get_listing`, and so on.
To undo: delete the `bolha` block from that file and restart Desktop.
### Optional environment variables
None are required. Add them under `"env"` in the config block, or pass
`--env KEY=value` to `claude mcp add`:
| Variable | Effect |
|---|---|
| `BOLHA_USER_AGENT` | Identify yourself to bolha.com (please do set this) |
| `BOLHA_CACHE_DIR` | Cache location (default `~/.bolha/cache`) |
| `BOLHA_NO_CACHE=1` | Disable caching |
| `BOLHA_VERBOSE=1` | Log requests to stderr |
Example with a custom user agent:
```bash
claude mcp add bolha --scope user \
--env BOLHA_USER_AGENT="janez/my-bolha-mcp" \
-- node D:\marketplace\bolha\dist\mcp\server.js
```
### Verifying from inside Claude
Ask it something that forces a live call, for example:
> Search bolha for "iPhone" under 500 EUR, cheapest first, and show me the
> listing URLs.
or, to check the connection specifically:
> List the bolha categories.
If the tools are not offered, the server did not start — check
`claude mcp list` for a connection error, and confirm `dist/mcp/server.js`
exists (run `npm run build`).
## Reliability
- **Polite by default** — descriptive `User-Agent`, ≤3 concurrent requests, ≥350 ms between
requests, retries with exponential backoff, per-request timeouts.
- **Breaks loudly** — if Bolha reports listings but none can be parsed, the client raises
`SITE_CHANGED` instead of quietly returning `[]`.
- **Bounded** — 500 listings and 100 pages per call, so an agent cannot accidentally trigger a
crawl.
- **Honest about limits** — see [`research/Known Limitations.md`](research/Known Limitations.md).
### robots.txt and ethics
Bolha's `robots.txt` defines 39 separate user-agent groups, and the details are easy to
misread. Checked directly against the live file:
| Path | `User-agent: *` | AI crawler groups |
|---|---|---|
| `/search`, `/hitro-iskanje`, `/brza-pretraga` | **Disallowed** | mixed |
| `/uporabnik/`, `/*-oglas-`, `/objava-oglasa` | not listed | Disallowed (GPTBot-style) |
So `/search` is off-limits to *every* crawler, while listing and profile URLs are only restricted
for training-oriented AI crawlers.
**How this project treats that.** It is a **user-operated client**: it runs on your machine, at
human pace, and returns results to your own terminal or agent. It does not train models, does not
systematically crawl, and does not re-publish content. Where an equivalent route exists, the
client prefers the **allowed** surface:
- `browse`/`category`/`filters`/`seller` all use **category URLs**, which are not disallowed;
- **public sitemaps** (`/sitemap-index.xml`) are the intended discovery surface;
- requests are paced (≥350 ms apart), capped at 3 concurrent, cached, and sent with a
descriptive `User-Agent` you can override via `BOLHA_USER_AGENT`;
- if Bolha serves a CAPTCHA or block, the client **reports it and stops** — it never tries to
defeat it.
Please keep it that way. If you need bulk discovery, the sitemaps are the right tool.
---
## Development
```bash
npm run build # compile TypeScript to dist/
npm test # unit tests (offline, 40 tests)
npm run test:mcp # MCP tests against the live site (23 checks)
npm run test:integration # live integration tests (21 tests)
npm run test:all # everything
```
Tests are real: unit tests run offline against sanitised fixtures, while integration and MCP
tests hit the live site read-only and are **discovery-based** (they search, then open whatever
came back) rather than depending on hardcoded listing ids that might expire.
### Project layout
```text
src/
types/ public types
parsing/ __INITIAL_STATE__ extraction + normalisers
core/ BolhaClient, HTTP, URLs, cache, errors
cli/ the bolha command
mcp/ MCP stdio server
research/ Obsidian knowledge base (start at research/INDEX.md)
tests/ unit (offline) · integration (live) · mcp (live)
```
---
## Documentation
| Document | Purpose |
|---|---|
| [`AGENTS.md`](AGENTS.md) | Project contract: rules, confirmed findings, decisions, status |
| [`BUGS.md`](BUGS.md) | Every bug found, with cause, fix and live evidence |
| [`STATUS.md`](STATUS.md) | What works and what has actually been tested |
| [`TODO.md`](TODO.md) | Remaining work |
| [`COMPLETION_REPORT.md`](COMPLETION_REPORT.md) | Final test report: every feature with PASS/FAIL/LIMITATION |
| [`research/INDEX.md`](research/INDEX.md) | Knowledge-base entry point |
| [`CONTRIBUTING.md`](CONTRIBUTING.md) | How to contribute, and the project's ground rules |
| [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) | Code of conduct |
| [`SECURITY.md`](SECURITY.md) | What counts as a security issue, and how to report one |
| [`CHANGELOG.md`](CHANGELOG.md) | Release history |
---
## Licence
MIT.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues