divar-mcp 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- divar_mcp-0.1.0/.github/workflows/ci.yml +22 -0
- divar_mcp-0.1.0/.github/workflows/publish.yml +51 -0
- divar_mcp-0.1.0/.gitignore +7 -0
- divar_mcp-0.1.0/CLAUDE.md +111 -0
- divar_mcp-0.1.0/LICENSE +21 -0
- divar_mcp-0.1.0/PKG-INFO +203 -0
- divar_mcp-0.1.0/README.md +177 -0
- divar_mcp-0.1.0/pyproject.toml +54 -0
- divar_mcp-0.1.0/server.json +32 -0
- divar_mcp-0.1.0/src/divar_mcp/__init__.py +5 -0
- divar_mcp-0.1.0/src/divar_mcp/catalog.py +359 -0
- divar_mcp-0.1.0/src/divar_mcp/core.py +888 -0
- divar_mcp-0.1.0/src/divar_mcp/http.py +115 -0
- divar_mcp-0.1.0/src/divar_mcp/market.py +221 -0
- divar_mcp-0.1.0/src/divar_mcp/meta.py +37 -0
- divar_mcp-0.1.0/src/divar_mcp/places.py +312 -0
- divar_mcp-0.1.0/src/divar_mcp/post.py +463 -0
- divar_mcp-0.1.0/src/divar_mcp/registry.py +19 -0
- divar_mcp-0.1.0/src/divar_mcp/search.py +714 -0
- divar_mcp-0.1.0/src/divar_mcp/server.py +186 -0
- divar_mcp-0.1.0/src/divar_mcp/store.py +35 -0
- divar_mcp-0.1.0/src/divar_mcp/watch.py +189 -0
- divar_mcp-0.1.0/tests/conftest.py +117 -0
- divar_mcp-0.1.0/tests/fixtures/brand_landing.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/business.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/categories.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/category_suggest.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/cities.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/count.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/districts.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/fields_search_brand.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/fields_search_districts.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/filters_light.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/filters_rent.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/find.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/geojson.json +50 -0
- divar_mcp-0.1.0/tests/fixtures/location_search.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/mapping.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/neighbours.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/options_brand.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/owner_posts.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/post_car.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/post_car2.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/post_flat_basement.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/post_flat_big.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/post_flat_bumped.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/post_flat_small.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/post_rent.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/reverse_geocode.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/search_empty.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/search_flats.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/search_light.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/search_light_p2.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/search_phones.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/search_pride.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/search_rent.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/search_sell.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/search_similar_flats.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/suggest.json +1 -0
- divar_mcp-0.1.0/tests/fixtures/viewport.json +1 -0
- divar_mcp-0.1.0/tests/test_catalog.py +110 -0
- divar_mcp-0.1.0/tests/test_core.py +74 -0
- divar_mcp-0.1.0/tests/test_http.py +150 -0
- divar_mcp-0.1.0/tests/test_live_catalog.py +56 -0
- divar_mcp-0.1.0/tests/test_live_places.py +38 -0
- divar_mcp-0.1.0/tests/test_live_post.py +81 -0
- divar_mcp-0.1.0/tests/test_live_realestate.py +47 -0
- divar_mcp-0.1.0/tests/test_live_search.py +129 -0
- divar_mcp-0.1.0/tests/test_market.py +93 -0
- divar_mcp-0.1.0/tests/test_places.py +115 -0
- divar_mcp-0.1.0/tests/test_post.py +195 -0
- divar_mcp-0.1.0/tests/test_realestate.py +251 -0
- divar_mcp-0.1.0/tests/test_search.py +332 -0
- divar_mcp-0.1.0/tests/test_watch.py +96 -0
- divar_mcp-0.1.0/uv.lock +1196 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python: ["3.10", "3.11", "3.12", "3.13"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v7
|
|
16
|
+
- uses: astral-sh/setup-uv@v10.2.0
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ matrix.python }}
|
|
19
|
+
- run: uv sync --locked
|
|
20
|
+
- run: uv run ruff check .
|
|
21
|
+
- run: uv run ruff format --check .
|
|
22
|
+
- run: uv run pytest -q
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
# Tag a release (git tag v0.1.0 && git push --tags) after bumping the version in
|
|
4
|
+
# pyproject.toml and server.json.
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
tags: ["v*"]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
pypi:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
environment: pypi
|
|
13
|
+
permissions:
|
|
14
|
+
id-token: write # PyPI trusted publishing
|
|
15
|
+
contents: write # GitHub Release
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v7
|
|
18
|
+
- uses: astral-sh/setup-uv@v10.2.0
|
|
19
|
+
- name: Check versions match the tag
|
|
20
|
+
run: |
|
|
21
|
+
TAG=${GITHUB_REF_NAME#v}
|
|
22
|
+
grep -q "^version = \"$TAG\"" pyproject.toml
|
|
23
|
+
test "$(jq -r .version server.json)" = "$TAG"
|
|
24
|
+
test "$(jq -r '.packages[0].version' server.json)" = "$TAG"
|
|
25
|
+
- run: uv sync --locked && uv run pytest -q
|
|
26
|
+
- run: uv build
|
|
27
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
28
|
+
- run: gh release create "$GITHUB_REF_NAME" dist/* --generate-notes --title "$GITHUB_REF_NAME"
|
|
29
|
+
env:
|
|
30
|
+
GH_TOKEN: ${{ github.token }}
|
|
31
|
+
|
|
32
|
+
mcp-registry:
|
|
33
|
+
needs: pypi
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
permissions:
|
|
36
|
+
id-token: write # MCP Registry login via GitHub OIDC
|
|
37
|
+
contents: read
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v7
|
|
40
|
+
- name: Wait for PyPI to serve the new version
|
|
41
|
+
run: |
|
|
42
|
+
for i in $(seq 1 30); do
|
|
43
|
+
curl -sf "https://pypi.org/pypi/divar-mcp/${GITHUB_REF_NAME#v}/json" >/dev/null && exit 0
|
|
44
|
+
sleep 10
|
|
45
|
+
done
|
|
46
|
+
exit 1
|
|
47
|
+
- name: Install mcp-publisher
|
|
48
|
+
run: |
|
|
49
|
+
curl -L "https://github.com/modelcontextprotocol/registry/releases/download/v1.8.1/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
|
|
50
|
+
- run: ./mcp-publisher login github-oidc
|
|
51
|
+
- run: ./mcp-publisher publish
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# divar-mcp
|
|
2
|
+
|
|
3
|
+
Unofficial, read-only MCP server for Divar (divar.ir), Iran's classifieds site (real estate, cars, phones, jobs, services).
|
|
4
|
+
Repo: GitHub `sepehr071/divar-mcp`. Not released yet: no PyPI or MCP Registry release (planned PyPI name `divar-mcp`).
|
|
5
|
+
It is a sibling of `../fidibo-mcp` and `../digikala-mcp` and copies their layout and idioms; keep them in step.
|
|
6
|
+
|
|
7
|
+
API map this server is built on (endpoints, samples, quirks, test report, competitor checklist): `../divar/`.
|
|
8
|
+
|
|
9
|
+
## Layout
|
|
10
|
+
|
|
11
|
+
- `src/divar_mcp/http.py`: the only HTTP code. One shared `httpx.AsyncClient` (`trust_env=False`, proxy only from
|
|
12
|
+
`DIVAR_MCP_PROXY`), a concurrency semaphore, `api(path, body=None)` for the gateway `https://api.divar.ir/v8` (GET, or
|
|
13
|
+
POST when `body` is given: every Divar POST used here is a read-only query), one retry (fixed 0.5 s, `RETRY_DELAY`) on
|
|
14
|
+
dropped connections, an empty 200 body and 502/503/504, no redirects followed, `ApiError(ToolError)` (passes on the API's
|
|
15
|
+
own `message` for 400/404), `set_transport()` for tests.
|
|
16
|
+
- `core.py`: not a tool module. Persian `fold`, price parsing (`to_int`, `short_toman`), `price_flags`, `row_prices`, `card`
|
|
17
|
+
(a POST_ROW -> compact ad), `dist` / `sane` statistics, cached reference data (cities, categories, filter schemas,
|
|
18
|
+
districts; 1 h, `_CACHE`), resolvers (`resolve_cities`, `resolve_category`, `resolve_districts`), `build_query` (friendly
|
|
19
|
+
args -> Divar search, reports `ignored` filters), `fetch_cards` (the paged loop) and the shared `Annotated` parameter types.
|
|
20
|
+
- `registry.py`: `@tool("Title")` collects tools. `server.py`: registers them with read-only annotations, holds `INSTRUCTIONS`.
|
|
21
|
+
- Tools: `places.py` (cities, districts, neighbours, locate, place search, mapping), `catalog.py` (categories, filters,
|
|
22
|
+
filter options, suggest), `search.py` (search, count, cheapest, best for budget, find deals, new since, export, nearby), `post.py`
|
|
23
|
+
(post, posts, compare, seller, dealer, similar; `parse_post`, `comparables`), `market.py` (market, appraise),
|
|
24
|
+
`watch.py` (saved watches, price trend; the only tools that write, to the local file of `store.py`), `meta.py` (status).
|
|
25
|
+
`places.py` also has `dv_boundary` (GeoJSON), `catalog.py` `dv_parse_query` (rule-based text -> dv_search args).
|
|
26
|
+
31 tools, all `dv_*`. `server.py` also registers MCP resources (`divar://cities`, `categories`, `category-filters/{slug}`)
|
|
27
|
+
and prompts. No `outputSchema` on purpose (a test checks it): tools return one compact JSON text block.
|
|
28
|
+
Token budget: `server._rows` trims every ad row on the way out (no `image` unless the call has `thumbnails=true`,
|
|
29
|
+
no `posted` next to `age_h`, no false/empty values), and `_untitled` flattens optional `anyOf [X, null]` schemas and
|
|
30
|
+
dedents descriptions. Shared parameter descriptions in `core.py` stay one short line: they repeat across 11 tools.
|
|
31
|
+
- `tests/`: `test_<module>.py` unit tests (MockTransport + trimmed real fixtures in `tests/fixtures/`, clock frozen in
|
|
32
|
+
`conftest.py`), `test_live_<group>.py` live tests (`@pytest.mark.live`), `test_http.py` for the client and the README tool tables.
|
|
33
|
+
|
|
34
|
+
## Commands
|
|
35
|
+
|
|
36
|
+
uv needs the local proxy to download packages; the server and tests call Divar directly (env proxies are ignored).
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
export HTTPS_PROXY=http://127.0.0.1:10898 HTTP_PROXY=http://127.0.0.1:10898
|
|
40
|
+
uv sync
|
|
41
|
+
uv run ruff check . && uv run ruff format --check .
|
|
42
|
+
uv run pytest -q # unit tests, offline
|
|
43
|
+
uv run pytest -m live -q # one live test per tool, about 25 s
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Rules for changes
|
|
47
|
+
|
|
48
|
+
- Read-only on Divar. The two `local=True` tools (`dv_watch`, `dv_price_trend`) write only `~/.divar-mcp/state.json`
|
|
49
|
+
(`DIVAR_MCP_STATE`) and are annotated not-read-only. Never call login/OTP (`/v5/auth`), `postcontact` (seller phone), chat, bookmarks, notes, ad submission or
|
|
50
|
+
drafts, payments, the business panel, or any analytics beacon (`/v1/client-exporter`). Seller phone numbers stay out of scope.
|
|
51
|
+
- Output units: prices in **Toman** (the API is Toman; nothing to convert). Card: `price` = sale price; rentals `deposit`
|
|
52
|
+
and `rent` (`rent` 0 + `full_deposit` = رهن کامل, from "رهن کامل" rows); no price key = negotiable (`negotiable` true when
|
|
53
|
+
the seller wrote توافقی or 0); `suspect_price` list = `tiny`, `placeholder`, `absurd`, `tiny_rent`. `slim` drops
|
|
54
|
+
None / false / empty values (0 stays; `ads` lists stay even when empty). Rankings use `main_price` (sale price, else
|
|
55
|
+
rent) and skip suspect prices; statistics also drop prices under a tenth of the median (`sane`).
|
|
56
|
+
- Tools never invent data Divar lacks: price ordering, appraisal, best value and trend are computed from fetched asking
|
|
57
|
+
prices and the answer says so (`scanned`, `comparable_ads`, `caveat`).
|
|
58
|
+
- Filters are validated against the category's filter schema (`filter_specs`, cached): unknown keys go to `ignored`,
|
|
59
|
+
because Divar silently drops them. Value shapes: number_range (strings), repeated_string, str, boolean.
|
|
60
|
+
- Slugs from API data go into paths only as validated tokens (`Token` pattern); user text is never put into a path.
|
|
61
|
+
- New tool: `@tool("Title")` async function, every param `Annotated[..., Field(...)]` with constraints and an example,
|
|
62
|
+
docstring = what it returns + when to use + what to call next, compact dict output. Add a unit test with a real trimmed
|
|
63
|
+
fixture, a live test, a README tool-table row, and an `INSTRUCTIONS` mention.
|
|
64
|
+
- Keep `README.md` tool tables equal to `list_tools()` (a unit test checks it). Deps stay `mcp` + `httpx` only.
|
|
65
|
+
- Write ZWNJ as `\u200c` in code, never the invisible character.
|
|
66
|
+
|
|
67
|
+
## Divar quirks (keep them)
|
|
68
|
+
|
|
69
|
+
- No auth, token or header needed. Unknown paths and wrong HTTP methods answer `403 RBAC: access denied`, not 404.
|
|
70
|
+
- List rows are protobuf-JSON widgets; `POST_ROW` is the ad card. Row prices are Persian-digit Toman strings; rent rows put
|
|
71
|
+
the deposit in `top_description_text` and rent in `middle_description_text` (`رهن کامل` = full deposit). The exact publish time
|
|
72
|
+
is `action_log.server_side_info.info.sort_date`; row text only has relative time.
|
|
73
|
+
- `POST /v8/postlist/w/search` is newest first only; the first 1-3 rows can be promoted. Paging: send
|
|
74
|
+
`pagination_data {"@type": "type.googleapis.com/post_list.PaginationData", "last_post_date": <cursor>, "page": 2, "layer_page": 2}`;
|
|
75
|
+
the cursor is the previous response's `pagination.data.last_post_date`. `page: N` alone returns page 1.
|
|
76
|
+
- Category slugs differ from web URL slugs (`car` = `light`, `buy-apartment` = `apartment-sell`); a wrong one is HTTP 400.
|
|
77
|
+
Web search URLs with API slugs 404 inconsistently (`house-villa-sell` 404s; `buy-apartment`, `rent-apartment`, `car`,
|
|
78
|
+
`mobile-phones` work), so `web_url` (`Q.web_url`) is built only for those categories, with `districts=1,2`, `price|credit|rent|size=min-max`
|
|
79
|
+
and `q=`, and is omitted for anything else.
|
|
80
|
+
- Post detail (`/posts-v2/web/{token}`): `webengage.price/credit/rent` are float32-rounded for big numbers
|
|
81
|
+
(2,670,000,000 arrives as 2,670,000,128). The exact numbers are in the `UNEXPANDABLE_ROW` "قیمت پایه" / "ودیعه" / "اجارهٔ ماهانه"
|
|
82
|
+
rows; `parse_post` prefers them.
|
|
83
|
+
- Filter option lists: `/v8/w/lazy-multi-select-hierarchy-options` (no `/postlist`), typed search `/v8/fields-search` (needs
|
|
84
|
+
`search_key`, e.g. `car_brand_model`; `count` is always 0). `reverse-geocode` needs the `location` key and 404s outside
|
|
85
|
+
covered districts. Dealer pages (`brand-landing`) give only the first 11 ads.
|
|
86
|
+
- Map pins are fuzzed about 500 m; map price chips are rounded text ("۱٫۵ میلیارد"), parsed by `short_toman`. Sale pins
|
|
87
|
+
have two chips: "قیمت:" (total, `price`) and "متری:" (per m2, `price_per_m2`). `mapview/viewport` returns nothing without a
|
|
88
|
+
category. Post detail sale ads (homes) put the exact total in the "قیمت کل" row.
|
|
89
|
+
- A province id (`parent` of a city) works as a search `city_ids` scope; there is no province list, so `city: "استان <city>"`
|
|
90
|
+
names the province of that city (no `web_url`: the site has no province address we could confirm). Typed brand search (`fields-search`) is fuzzy ('car' finds Kia Carens): `build_query`
|
|
91
|
+
keeps only hits that contain the typed text and validates keys against the option tree.
|
|
92
|
+
- Business card of a personal ad: `business-data/{token}/lazy` answers 404 "business not found"; `dv_seller` treats that as
|
|
93
|
+
a personal seller. `/places/districts/{id}/neighbours` is short (Vanak lists only Jordan), so `dv_neighbours` adds `nearest`
|
|
94
|
+
by centroid distance. `webengage.business_ref` (17 chars) is not a `brand_token`; `dv_post` does not return it.
|
|
95
|
+
- `dv_best_for_budget` / `dv_find_deals` skip ads under 20% of the uncapped market median and bait titles (`_IMPLAUSIBLE`, `_BAIT`, unless the
|
|
96
|
+
word is in the query). Property for sale is ranked per m2: list rows have no area, so it comes from the title, else from the ad page
|
|
97
|
+
(`with_areas`, at most 6 fetches: ~30 rapid post fetches got HTTP 429). Per-m2 values are cut to whole thousands like the site (`per_m2`).
|
|
98
|
+
- `sort_date` / `age_h` is the last bump (نردبان), not the publication: only the ad page has the real time ("انتشار آگهی" /
|
|
99
|
+
"آخرین نردبان" Jalali lines, `published_at` / `bumped_at` via `jalali_time`). `rank_pool` opens the pages of the 6 best per-m2
|
|
100
|
+
ads to read it plus floor (زیرهمکف), "پارکینگ ندارد", "آسانسور ندارد", and rescales the score; unchecked ads get only +5 for a
|
|
101
|
+
recent bump. `dv_similar` drops property cards priced 5x off or with a title area 0.4x-2.5x off (`excluded_outliers`).
|
|
102
|
+
- `dv_search` flags prices at or under a tenth of the fetched median as `outlier` (`flag_outliers`, needs 5 priced ads).
|
|
103
|
+
- No rate limit was seen (40 searches in 15 s). Keep calls modest anyway: the semaphore allows 4 at once.
|
|
104
|
+
- Not available: price valuation, a similar-ads endpoint (`dv_similar` searches the ad's breadcrumb), sort by price,
|
|
105
|
+
dealer paging, SEO slug routes, anything behind login.
|
|
106
|
+
|
|
107
|
+
## Release (when it is time)
|
|
108
|
+
|
|
109
|
+
Bump `version` in `pyproject.toml` and both versions in `server.json`, `git init`, create the GitHub repo, tag `vX.Y.Z`.
|
|
110
|
+
`.github/workflows/publish.yml` (copied from fidibo-mcp, untested here) publishes to PyPI and the MCP Registry.
|
|
111
|
+
Then check the server table in `../README.md` and `../../awesome-iran-mcp` (see `../CLAUDE.md`).
|
divar_mcp-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sepehr Radmard
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
divar_mcp-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: divar-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Unofficial read-only MCP server for Divar (divar.ir): search classified ads in any Iranian city, compare, find the cheapest and best deals, appraise prices, browse the map.
|
|
5
|
+
Project-URL: Homepage, https://github.com/sepehr071/divar-mcp
|
|
6
|
+
Project-URL: Source, https://github.com/sepehr071/divar-mcp
|
|
7
|
+
Project-URL: Changelog, https://github.com/sepehr071/divar-mcp/releases
|
|
8
|
+
Project-URL: Issues, https://github.com/sepehr071/divar-mcp/issues
|
|
9
|
+
Author-email: Sepehr Radmard <sepehrr80@gmail.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: cars,classifieds,divar,iran,mcp,model-context-protocol,persian,real-estate
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: httpx>=0.27
|
|
24
|
+
Requires-Dist: mcp<3,>=2.2
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
<!-- mcp-name: io.github.sepehr071/divar-mcp -->
|
|
28
|
+
|
|
29
|
+
# divar-mcp: Divar MCP server for AI agents
|
|
30
|
+
|
|
31
|
+
**Unofficial, read-only MCP server that lets your AI agent search Divar (دیوار) ads in any Iranian city: homes, cars, phones and more. It finds the cheapest and best-value ads, compares ads, checks whether a price is fair and browses the map. Prices are in Toman.**
|
|
32
|
+
|
|
33
|
+
An [MCP](https://modelcontextprotocol.io) server for [Divar](https://divar.ir), Iran's largest classifieds site.
|
|
34
|
+
Search by city, neighbourhood, category, text and any of the category's filters, read a full ad, compare several, see
|
|
35
|
+
the seller's other ads, get market statistics, appraise a price against similar ads and poll for new ads, all from
|
|
36
|
+
Claude, Cursor or Copilot. No account, no API key, nothing is posted or changed.
|
|
37
|
+
|
|
38
|
+
[Quick start](#quick-start) · [Tools](#tools) · [FAQ](#faq) · [فارسی](#فارسی)
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## For AI agents
|
|
43
|
+
|
|
44
|
+
- **What:** Unofficial MCP server for Divar (دیوار, divar.ir), Iran's classifieds site. Persian queries work best; prices in Toman.
|
|
45
|
+
- **Install:** `claude mcp add divar -- uvx divar-mcp` (stdio, needs [uv](https://docs.astral.sh/uv/getting-started/installation/) and Python 3.10+).
|
|
46
|
+
- **Auth:** none. Optional `DIVAR_MCP_PROXY` sets an HTTP proxy for every request.
|
|
47
|
+
- **Access:** read-only on Divar. It cannot log in, post, chat, bookmark or reveal a seller's phone number. Only `dv_watch` and `dv_price_trend` write, to a local file of your own searches.
|
|
48
|
+
- **Also:** resources `divar://cities`, `divar://categories`, `divar://category-filters/{slug}`; prompts `compare_ads`, `best_under_budget`, `appraise_listing`, `watch_market`.
|
|
49
|
+
- **Tools:** 31, all prefixed `dv_`. Start with `dv_search`, `dv_post`, `dv_cheapest`, `dv_best_for_budget`, `dv_market`.
|
|
50
|
+
- **Typical sequence:** `dv_cities` / `dv_categories` -> `dv_filters` -> `dv_search` -> `dv_post` -> `dv_similar` / `dv_appraise` / `dv_compare`.
|
|
51
|
+
- **Use when:** the user wants to find, price-check or monitor Divar ads (flats to buy or rent, used cars, phones, furniture, jobs...), or asks what something costs in a city.
|
|
52
|
+
- **Do not use when:** the user wants to post an ad, message a seller or get a phone number (login-only on Divar).
|
|
53
|
+
- **Output:** compact JSON ad cards. `price` for a sale; `deposit` + `rent` for a rental; no price means negotiable. Pass `token` between tools and `next_cursor` back as `cursor`.
|
|
54
|
+
- **Machine-readable:** [server.json](https://github.com/sepehr071/divar-mcp/blob/main/server.json), and the catalog of all Iranian MCP servers [awesome-iran-mcp](https://github.com/sepehr071/awesome-iran-mcp) (`TOOLS.md`, `servers.json`, `llms.txt`).
|
|
55
|
+
|
|
56
|
+
## Quick start
|
|
57
|
+
|
|
58
|
+
You need [uv](https://docs.astral.sh/uv/getting-started/installation/).
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
claude mcp add divar -- uvx divar-mcp
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Any other MCP client: command `uvx`, args `["divar-mcp"]` (or `pip install divar-mcp` and run `divar-mcp`).
|
|
65
|
+
|
|
66
|
+
## What it can do
|
|
67
|
+
|
|
68
|
+
- Search ads in one or several cities by category, text, neighbourhood, price (and deposit for rentals), brand and model, photo, freshness and every other filter of the category; merge several pages with duplicates removed; continue with a cursor
|
|
69
|
+
- Full ad: price in **Toman**, specs, features, description, all images, map position, seller type, shared-housing flag, placeholder-price detection
|
|
70
|
+
- Smart tools: cheapest ads (junk prices skipped), best value and most-for-the-budget, side-by-side comparison showing only the differences, similar ads, fair-price appraisal with percentile and confidence, market statistics by district and by day
|
|
71
|
+
- Monitoring: poll `dv_new_since` with the last timestamp to see only new ads; export results as CSV or JSON lines
|
|
72
|
+
- Map: ads around a GPS point or landmark with distances; neighbourhoods, neighbours, address of a point
|
|
73
|
+
- Sellers and dealers: how many ads a seller has, the business card and its ads
|
|
74
|
+
|
|
75
|
+
## Tools
|
|
76
|
+
|
|
77
|
+
All tools are read-only and start with `dv_`. An ad card has `token`, `title`, `price` (sale) or `deposit` + `rent`
|
|
78
|
+
(`rent` 0 with `full_deposit` means رهن کامل), `info` (mileage, condition...), `district`, `seller` (agency label),
|
|
79
|
+
`age_h` (hours since the last bump), `photos`, `image` (thumbnail URL, only from `dv_search` with `thumbnails`), `chat`, `tag` (paid promotion), `url`, and
|
|
80
|
+
`suspect_price` when the price looks like a placeholder. Absent flags mean false. Default page size is 10 to 20 ads.
|
|
81
|
+
|
|
82
|
+
### Find places and categories
|
|
83
|
+
|
|
84
|
+
| Tool | What it does |
|
|
85
|
+
|---|---|
|
|
86
|
+
| `dv_cities` | City ids for a name (1,177 cities); no query lists the biggest cities |
|
|
87
|
+
| `dv_districts` | Neighbourhoods of a city with ids, centre and main streets |
|
|
88
|
+
| `dv_neighbours` | Neighbourhoods adjacent to a district (Divar's list plus the nearest ones by distance) |
|
|
89
|
+
| `dv_locate` | City, neighbourhood and street address of a GPS point |
|
|
90
|
+
| `dv_place_search` | Map autocomplete: landmark or street to coordinates |
|
|
91
|
+
| `dv_place_mapping` | Convert old Divar place ids to current ones |
|
|
92
|
+
| `dv_categories` | The 236-node category tree; search by name, list children |
|
|
93
|
+
| `dv_filters` | Every filter of a category: key, type, label, unit, options |
|
|
94
|
+
| `dv_filter_options` | Brand and model keys (161 car brands), districts and other option lists, with typed search |
|
|
95
|
+
| `dv_suggest` | The search box's suggestions and category guesses for some text |
|
|
96
|
+
| `dv_parse_query` | A plain Persian request ("آپارتمان اجاره ونک زیر ۸ میلیارد") to `dv_search` arguments: city, neighbourhoods, price bounds, area (`80 متری`), category |
|
|
97
|
+
| `dv_boundary` | Neighbourhood borders of a city (GeoJSON): the city box, or one neighbourhood's polygon |
|
|
98
|
+
|
|
99
|
+
### Search and price
|
|
100
|
+
|
|
101
|
+
| Tool | What it does |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `dv_search` | Search ads with city, category, text, districts, price, brand, photo, freshness and any category filter; multi-page merge, client-side age and keyword filters, cursor paging |
|
|
104
|
+
| `dv_count` | How many ads match a search (capped at 10,000) |
|
|
105
|
+
| `dv_cheapest` | The cheapest ads of a search; placeholder prices and far-below-median outliers skipped |
|
|
106
|
+
| `dv_best_for_budget` | Within a budget: best value with an explainable `deal_score` (and `reasons`) against the median of the ads that fit (price per m2 for property for sale), and the most you can get; bait / co-op / loan titles are skipped |
|
|
107
|
+
| `dv_find_deals` | Ads priced well below the median of a search, no budget needed; junk prices and bait titles removed |
|
|
108
|
+
| `dv_market` | Price statistics (min, quartiles, median, max) plus breakdown by district and by day |
|
|
109
|
+
| `dv_appraise` | Is an ad's price (or a price you give) high or low versus similar ads: percentile, verdict ("insufficient comparables" when the call is extreme but thin), confidence |
|
|
110
|
+
| `dv_new_since` | Ads published after a timestamp: poll with the returned `latest` to watch a search |
|
|
111
|
+
| `dv_export` | The ads of a search as CSV or JSON lines |
|
|
112
|
+
| `dv_nearby` | Map ads around a point (category required), nearest first, with coordinates, distance and total price |
|
|
113
|
+
| `dv_watch` | Saved searches: create, then check to get only the ads newer than the last check (local file) |
|
|
114
|
+
| `dv_price_trend` | One median asking price per day for a search, kept locally, with change since the previous day |
|
|
115
|
+
| `dv_status` | Is Divar reachable, latency, caches, saved watches and the tool list |
|
|
116
|
+
|
|
117
|
+
### One ad and its seller
|
|
118
|
+
|
|
119
|
+
| Tool | What it does |
|
|
120
|
+
|---|---|
|
|
121
|
+
| `dv_post` | Full ad: price, specs, features, description, images, map position, seller type, flags |
|
|
122
|
+
| `dv_posts` | Up to 10 ads in one call, compact |
|
|
123
|
+
| `dv_compare` | 2 to 6 ads side by side: prices and only the specs that differ |
|
|
124
|
+
| `dv_similar` | Ads like this one (same brand and area, widening step by step) |
|
|
125
|
+
| `dv_seller` | A seller's personal and business ad counts, business card (or "personal seller") and other ads |
|
|
126
|
+
| `dv_dealer` | A business's public page: name, member since, first 11 ads |
|
|
127
|
+
|
|
128
|
+
## Good to know
|
|
129
|
+
|
|
130
|
+
- **Prices are Toman.** Divar's prices are Toman already; nothing is converted. A missing price means negotiable (توافقی), never 0. Rentals have `deposit` (ودیعه) and `rent` (اجاره); `rent` 0 with `full_deposit` is رهن کامل.
|
|
131
|
+
- **Asking prices.** Everything is what sellers typed, not what deals close at. Some sellers type junk (1,111,111 or a deposit as a car price): `suspect_price` flags placeholders, and rankings and statistics skip them and prices under a tenth of the median.
|
|
132
|
+
- **Province scope.** `city: "استان مشهد"` (or `province:مشهد`) searches the whole province of that city; districts and boundaries need a city.
|
|
133
|
+
- **Order.** Divar's API only returns newest first; the first 1-3 rows can be paid promotions with older dates. `dv_search` `sort` and the price tools sort the ads fetched in the call, not the whole market. Paging is a time cursor (`next_cursor` -> `cursor`).
|
|
134
|
+
- **Filters.** `dv_filters` lists the keys of a category; filters a category lacks are reported in `ignored`, not silently dropped. Web URL slugs (`car`) work next to API slugs (`light`).
|
|
135
|
+
- **Map positions** of most ads are fuzzed by about 500 m (`approximate_location`).
|
|
136
|
+
- **No phone numbers.** Divar shows them only to logged-in users; this server never logs in. Use `url` to open the ad.
|
|
137
|
+
- Detail prices come from the ad's price rows; Divar's raw number elsewhere in the ad is float-rounded for large values.
|
|
138
|
+
- Persian queries work best; Arabic letter variants (ي/ك) are folded, and an empty Persian query is retried once folded.
|
|
139
|
+
|
|
140
|
+
## FAQ
|
|
141
|
+
|
|
142
|
+
**Is there a Divar API or MCP server for AI agents?**
|
|
143
|
+
Yes: this unofficial MCP server wraps the public JSON API the Divar website itself uses, so Claude, Cursor or
|
|
144
|
+
Copilot can search ads, read details and compare prices. Divar's official Kenar API is for partners (OAuth and an
|
|
145
|
+
API key) and is not used here.
|
|
146
|
+
|
|
147
|
+
**Is it official, safe and free?**
|
|
148
|
+
It is unofficial, open source (MIT) and free. It is read-only, needs no account or API key, and runs on your
|
|
149
|
+
machine with no hosted server in between.
|
|
150
|
+
|
|
151
|
+
**Can it message a seller, post an ad or get a phone number?**
|
|
152
|
+
No. Those need a Divar login and are deliberately left out.
|
|
153
|
+
|
|
154
|
+
**Can it alert me when a matching ad appears?**
|
|
155
|
+
It does not push. `dv_new_since` returns only ads newer than a timestamp, and `dv_watch` remembers that timestamp for a saved search (in `~/.divar-mcp/state.json`, or `DIVAR_MCP_STATE`), so a scheduler (cron, a Claude Code loop) can call `dv_watch` with `action: "check"`.
|
|
156
|
+
|
|
157
|
+
## Configuration
|
|
158
|
+
|
|
159
|
+
The server calls `api.divar.ir` directly and ignores system proxy variables. To use a proxy set `DIVAR_MCP_PROXY=http://host:port`.
|
|
160
|
+
Saved watches and price history go to `~/.divar-mcp/state.json`; set `DIVAR_MCP_STATE` to use another file.
|
|
161
|
+
|
|
162
|
+
## Development
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
export HTTPS_PROXY=http://127.0.0.1:10898 HTTP_PROXY=http://127.0.0.1:10898 # uv downloads only
|
|
166
|
+
uv sync
|
|
167
|
+
uv run ruff check . && uv run ruff format --check .
|
|
168
|
+
uv run pytest -q # offline, recorded fixtures
|
|
169
|
+
uv run pytest -m live -q # real API, one test per tool
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The API map this server is built on lives next to it in `../divar/`.
|
|
173
|
+
|
|
174
|
+
<a id="فارسی"></a>
|
|
175
|
+
|
|
176
|
+
## فارسی: سرور MCP دیوار
|
|
177
|
+
|
|
178
|
+
<div dir="rtl">
|
|
179
|
+
|
|
180
|
+
**divar-mcp** یک سرور MCP غیررسمی برای دیوار است که به دستیار هوش مصنوعی شما (Claude، Cursor، Copilot و ...) اجازه
|
|
181
|
+
می‌دهد در آگهی‌های هر شهر ایران جستجو کند، آگهی را کامل ببیند، چند آگهی را مقایسه کند، ارزان‌ترین و
|
|
182
|
+
مناسب‌ترین آگهی را برای یک بودجه پیدا کند، منصفانه بودن قیمت را بسنجد و آگهی‌های اطراف یک نقطه روی نقشه را ببیند.
|
|
183
|
+
|
|
184
|
+
- فقط خواندنی است و حساب کاربری یا کلید API نمی‌خواهد؛ شماره تلفن فروشنده را نشان نمی‌دهد.
|
|
185
|
+
- همه قیمت‌ها به تومان است؛ قیمت نداشتن یعنی توافقی.
|
|
186
|
+
|
|
187
|
+
بپرسید: «ارزان‌ترین آپارتمان ۲ خوابه اجاره‌ای در ونک با ودیعه زیر ۱ میلیارد کدام است؟»
|
|
188
|
+
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
## Related servers
|
|
192
|
+
|
|
193
|
+
- All Iranian MCP servers: [awesome-iran-mcp](https://github.com/sepehr071/awesome-iran-mcp)
|
|
194
|
+
|
|
195
|
+
## Disclaimer
|
|
196
|
+
|
|
197
|
+
Unofficial; not affiliated with Divar. Read-only access to the data of public pages. Keep request rates low (Divar showed no rate limit, but be polite).
|
|
198
|
+
|
|
199
|
+
If this saves you clicks, a star helps others find it.
|
|
200
|
+
|
|
201
|
+
## License
|
|
202
|
+
|
|
203
|
+
MIT
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
<!-- mcp-name: io.github.sepehr071/divar-mcp -->
|
|
2
|
+
|
|
3
|
+
# divar-mcp: Divar MCP server for AI agents
|
|
4
|
+
|
|
5
|
+
**Unofficial, read-only MCP server that lets your AI agent search Divar (دیوار) ads in any Iranian city: homes, cars, phones and more. It finds the cheapest and best-value ads, compares ads, checks whether a price is fair and browses the map. Prices are in Toman.**
|
|
6
|
+
|
|
7
|
+
An [MCP](https://modelcontextprotocol.io) server for [Divar](https://divar.ir), Iran's largest classifieds site.
|
|
8
|
+
Search by city, neighbourhood, category, text and any of the category's filters, read a full ad, compare several, see
|
|
9
|
+
the seller's other ads, get market statistics, appraise a price against similar ads and poll for new ads, all from
|
|
10
|
+
Claude, Cursor or Copilot. No account, no API key, nothing is posted or changed.
|
|
11
|
+
|
|
12
|
+
[Quick start](#quick-start) · [Tools](#tools) · [FAQ](#faq) · [فارسی](#فارسی)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## For AI agents
|
|
17
|
+
|
|
18
|
+
- **What:** Unofficial MCP server for Divar (دیوار, divar.ir), Iran's classifieds site. Persian queries work best; prices in Toman.
|
|
19
|
+
- **Install:** `claude mcp add divar -- uvx divar-mcp` (stdio, needs [uv](https://docs.astral.sh/uv/getting-started/installation/) and Python 3.10+).
|
|
20
|
+
- **Auth:** none. Optional `DIVAR_MCP_PROXY` sets an HTTP proxy for every request.
|
|
21
|
+
- **Access:** read-only on Divar. It cannot log in, post, chat, bookmark or reveal a seller's phone number. Only `dv_watch` and `dv_price_trend` write, to a local file of your own searches.
|
|
22
|
+
- **Also:** resources `divar://cities`, `divar://categories`, `divar://category-filters/{slug}`; prompts `compare_ads`, `best_under_budget`, `appraise_listing`, `watch_market`.
|
|
23
|
+
- **Tools:** 31, all prefixed `dv_`. Start with `dv_search`, `dv_post`, `dv_cheapest`, `dv_best_for_budget`, `dv_market`.
|
|
24
|
+
- **Typical sequence:** `dv_cities` / `dv_categories` -> `dv_filters` -> `dv_search` -> `dv_post` -> `dv_similar` / `dv_appraise` / `dv_compare`.
|
|
25
|
+
- **Use when:** the user wants to find, price-check or monitor Divar ads (flats to buy or rent, used cars, phones, furniture, jobs...), or asks what something costs in a city.
|
|
26
|
+
- **Do not use when:** the user wants to post an ad, message a seller or get a phone number (login-only on Divar).
|
|
27
|
+
- **Output:** compact JSON ad cards. `price` for a sale; `deposit` + `rent` for a rental; no price means negotiable. Pass `token` between tools and `next_cursor` back as `cursor`.
|
|
28
|
+
- **Machine-readable:** [server.json](https://github.com/sepehr071/divar-mcp/blob/main/server.json), and the catalog of all Iranian MCP servers [awesome-iran-mcp](https://github.com/sepehr071/awesome-iran-mcp) (`TOOLS.md`, `servers.json`, `llms.txt`).
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
You need [uv](https://docs.astral.sh/uv/getting-started/installation/).
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
claude mcp add divar -- uvx divar-mcp
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Any other MCP client: command `uvx`, args `["divar-mcp"]` (or `pip install divar-mcp` and run `divar-mcp`).
|
|
39
|
+
|
|
40
|
+
## What it can do
|
|
41
|
+
|
|
42
|
+
- Search ads in one or several cities by category, text, neighbourhood, price (and deposit for rentals), brand and model, photo, freshness and every other filter of the category; merge several pages with duplicates removed; continue with a cursor
|
|
43
|
+
- Full ad: price in **Toman**, specs, features, description, all images, map position, seller type, shared-housing flag, placeholder-price detection
|
|
44
|
+
- Smart tools: cheapest ads (junk prices skipped), best value and most-for-the-budget, side-by-side comparison showing only the differences, similar ads, fair-price appraisal with percentile and confidence, market statistics by district and by day
|
|
45
|
+
- Monitoring: poll `dv_new_since` with the last timestamp to see only new ads; export results as CSV or JSON lines
|
|
46
|
+
- Map: ads around a GPS point or landmark with distances; neighbourhoods, neighbours, address of a point
|
|
47
|
+
- Sellers and dealers: how many ads a seller has, the business card and its ads
|
|
48
|
+
|
|
49
|
+
## Tools
|
|
50
|
+
|
|
51
|
+
All tools are read-only and start with `dv_`. An ad card has `token`, `title`, `price` (sale) or `deposit` + `rent`
|
|
52
|
+
(`rent` 0 with `full_deposit` means رهن کامل), `info` (mileage, condition...), `district`, `seller` (agency label),
|
|
53
|
+
`age_h` (hours since the last bump), `photos`, `image` (thumbnail URL, only from `dv_search` with `thumbnails`), `chat`, `tag` (paid promotion), `url`, and
|
|
54
|
+
`suspect_price` when the price looks like a placeholder. Absent flags mean false. Default page size is 10 to 20 ads.
|
|
55
|
+
|
|
56
|
+
### Find places and categories
|
|
57
|
+
|
|
58
|
+
| Tool | What it does |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `dv_cities` | City ids for a name (1,177 cities); no query lists the biggest cities |
|
|
61
|
+
| `dv_districts` | Neighbourhoods of a city with ids, centre and main streets |
|
|
62
|
+
| `dv_neighbours` | Neighbourhoods adjacent to a district (Divar's list plus the nearest ones by distance) |
|
|
63
|
+
| `dv_locate` | City, neighbourhood and street address of a GPS point |
|
|
64
|
+
| `dv_place_search` | Map autocomplete: landmark or street to coordinates |
|
|
65
|
+
| `dv_place_mapping` | Convert old Divar place ids to current ones |
|
|
66
|
+
| `dv_categories` | The 236-node category tree; search by name, list children |
|
|
67
|
+
| `dv_filters` | Every filter of a category: key, type, label, unit, options |
|
|
68
|
+
| `dv_filter_options` | Brand and model keys (161 car brands), districts and other option lists, with typed search |
|
|
69
|
+
| `dv_suggest` | The search box's suggestions and category guesses for some text |
|
|
70
|
+
| `dv_parse_query` | A plain Persian request ("آپارتمان اجاره ونک زیر ۸ میلیارد") to `dv_search` arguments: city, neighbourhoods, price bounds, area (`80 متری`), category |
|
|
71
|
+
| `dv_boundary` | Neighbourhood borders of a city (GeoJSON): the city box, or one neighbourhood's polygon |
|
|
72
|
+
|
|
73
|
+
### Search and price
|
|
74
|
+
|
|
75
|
+
| Tool | What it does |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `dv_search` | Search ads with city, category, text, districts, price, brand, photo, freshness and any category filter; multi-page merge, client-side age and keyword filters, cursor paging |
|
|
78
|
+
| `dv_count` | How many ads match a search (capped at 10,000) |
|
|
79
|
+
| `dv_cheapest` | The cheapest ads of a search; placeholder prices and far-below-median outliers skipped |
|
|
80
|
+
| `dv_best_for_budget` | Within a budget: best value with an explainable `deal_score` (and `reasons`) against the median of the ads that fit (price per m2 for property for sale), and the most you can get; bait / co-op / loan titles are skipped |
|
|
81
|
+
| `dv_find_deals` | Ads priced well below the median of a search, no budget needed; junk prices and bait titles removed |
|
|
82
|
+
| `dv_market` | Price statistics (min, quartiles, median, max) plus breakdown by district and by day |
|
|
83
|
+
| `dv_appraise` | Is an ad's price (or a price you give) high or low versus similar ads: percentile, verdict ("insufficient comparables" when the call is extreme but thin), confidence |
|
|
84
|
+
| `dv_new_since` | Ads published after a timestamp: poll with the returned `latest` to watch a search |
|
|
85
|
+
| `dv_export` | The ads of a search as CSV or JSON lines |
|
|
86
|
+
| `dv_nearby` | Map ads around a point (category required), nearest first, with coordinates, distance and total price |
|
|
87
|
+
| `dv_watch` | Saved searches: create, then check to get only the ads newer than the last check (local file) |
|
|
88
|
+
| `dv_price_trend` | One median asking price per day for a search, kept locally, with change since the previous day |
|
|
89
|
+
| `dv_status` | Is Divar reachable, latency, caches, saved watches and the tool list |
|
|
90
|
+
|
|
91
|
+
### One ad and its seller
|
|
92
|
+
|
|
93
|
+
| Tool | What it does |
|
|
94
|
+
|---|---|
|
|
95
|
+
| `dv_post` | Full ad: price, specs, features, description, images, map position, seller type, flags |
|
|
96
|
+
| `dv_posts` | Up to 10 ads in one call, compact |
|
|
97
|
+
| `dv_compare` | 2 to 6 ads side by side: prices and only the specs that differ |
|
|
98
|
+
| `dv_similar` | Ads like this one (same brand and area, widening step by step) |
|
|
99
|
+
| `dv_seller` | A seller's personal and business ad counts, business card (or "personal seller") and other ads |
|
|
100
|
+
| `dv_dealer` | A business's public page: name, member since, first 11 ads |
|
|
101
|
+
|
|
102
|
+
## Good to know
|
|
103
|
+
|
|
104
|
+
- **Prices are Toman.** Divar's prices are Toman already; nothing is converted. A missing price means negotiable (توافقی), never 0. Rentals have `deposit` (ودیعه) and `rent` (اجاره); `rent` 0 with `full_deposit` is رهن کامل.
|
|
105
|
+
- **Asking prices.** Everything is what sellers typed, not what deals close at. Some sellers type junk (1,111,111 or a deposit as a car price): `suspect_price` flags placeholders, and rankings and statistics skip them and prices under a tenth of the median.
|
|
106
|
+
- **Province scope.** `city: "استان مشهد"` (or `province:مشهد`) searches the whole province of that city; districts and boundaries need a city.
|
|
107
|
+
- **Order.** Divar's API only returns newest first; the first 1-3 rows can be paid promotions with older dates. `dv_search` `sort` and the price tools sort the ads fetched in the call, not the whole market. Paging is a time cursor (`next_cursor` -> `cursor`).
|
|
108
|
+
- **Filters.** `dv_filters` lists the keys of a category; filters a category lacks are reported in `ignored`, not silently dropped. Web URL slugs (`car`) work next to API slugs (`light`).
|
|
109
|
+
- **Map positions** of most ads are fuzzed by about 500 m (`approximate_location`).
|
|
110
|
+
- **No phone numbers.** Divar shows them only to logged-in users; this server never logs in. Use `url` to open the ad.
|
|
111
|
+
- Detail prices come from the ad's price rows; Divar's raw number elsewhere in the ad is float-rounded for large values.
|
|
112
|
+
- Persian queries work best; Arabic letter variants (ي/ك) are folded, and an empty Persian query is retried once folded.
|
|
113
|
+
|
|
114
|
+
## FAQ
|
|
115
|
+
|
|
116
|
+
**Is there a Divar API or MCP server for AI agents?**
|
|
117
|
+
Yes: this unofficial MCP server wraps the public JSON API the Divar website itself uses, so Claude, Cursor or
|
|
118
|
+
Copilot can search ads, read details and compare prices. Divar's official Kenar API is for partners (OAuth and an
|
|
119
|
+
API key) and is not used here.
|
|
120
|
+
|
|
121
|
+
**Is it official, safe and free?**
|
|
122
|
+
It is unofficial, open source (MIT) and free. It is read-only, needs no account or API key, and runs on your
|
|
123
|
+
machine with no hosted server in between.
|
|
124
|
+
|
|
125
|
+
**Can it message a seller, post an ad or get a phone number?**
|
|
126
|
+
No. Those need a Divar login and are deliberately left out.
|
|
127
|
+
|
|
128
|
+
**Can it alert me when a matching ad appears?**
|
|
129
|
+
It does not push. `dv_new_since` returns only ads newer than a timestamp, and `dv_watch` remembers that timestamp for a saved search (in `~/.divar-mcp/state.json`, or `DIVAR_MCP_STATE`), so a scheduler (cron, a Claude Code loop) can call `dv_watch` with `action: "check"`.
|
|
130
|
+
|
|
131
|
+
## Configuration
|
|
132
|
+
|
|
133
|
+
The server calls `api.divar.ir` directly and ignores system proxy variables. To use a proxy set `DIVAR_MCP_PROXY=http://host:port`.
|
|
134
|
+
Saved watches and price history go to `~/.divar-mcp/state.json`; set `DIVAR_MCP_STATE` to use another file.
|
|
135
|
+
|
|
136
|
+
## Development
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
export HTTPS_PROXY=http://127.0.0.1:10898 HTTP_PROXY=http://127.0.0.1:10898 # uv downloads only
|
|
140
|
+
uv sync
|
|
141
|
+
uv run ruff check . && uv run ruff format --check .
|
|
142
|
+
uv run pytest -q # offline, recorded fixtures
|
|
143
|
+
uv run pytest -m live -q # real API, one test per tool
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The API map this server is built on lives next to it in `../divar/`.
|
|
147
|
+
|
|
148
|
+
<a id="فارسی"></a>
|
|
149
|
+
|
|
150
|
+
## فارسی: سرور MCP دیوار
|
|
151
|
+
|
|
152
|
+
<div dir="rtl">
|
|
153
|
+
|
|
154
|
+
**divar-mcp** یک سرور MCP غیررسمی برای دیوار است که به دستیار هوش مصنوعی شما (Claude، Cursor، Copilot و ...) اجازه
|
|
155
|
+
می‌دهد در آگهی‌های هر شهر ایران جستجو کند، آگهی را کامل ببیند، چند آگهی را مقایسه کند، ارزان‌ترین و
|
|
156
|
+
مناسب‌ترین آگهی را برای یک بودجه پیدا کند، منصفانه بودن قیمت را بسنجد و آگهی‌های اطراف یک نقطه روی نقشه را ببیند.
|
|
157
|
+
|
|
158
|
+
- فقط خواندنی است و حساب کاربری یا کلید API نمی‌خواهد؛ شماره تلفن فروشنده را نشان نمی‌دهد.
|
|
159
|
+
- همه قیمت‌ها به تومان است؛ قیمت نداشتن یعنی توافقی.
|
|
160
|
+
|
|
161
|
+
بپرسید: «ارزان‌ترین آپارتمان ۲ خوابه اجاره‌ای در ونک با ودیعه زیر ۱ میلیارد کدام است؟»
|
|
162
|
+
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
## Related servers
|
|
166
|
+
|
|
167
|
+
- All Iranian MCP servers: [awesome-iran-mcp](https://github.com/sepehr071/awesome-iran-mcp)
|
|
168
|
+
|
|
169
|
+
## Disclaimer
|
|
170
|
+
|
|
171
|
+
Unofficial; not affiliated with Divar. Read-only access to the data of public pages. Keep request rates low (Divar showed no rate limit, but be polite).
|
|
172
|
+
|
|
173
|
+
If this saves you clicks, a star helps others find it.
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
MIT
|