arabam-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.
- arabam_mcp-0.1.0/.github/workflows/publish.yml +20 -0
- arabam_mcp-0.1.0/.github/workflows/registry.yml +61 -0
- arabam_mcp-0.1.0/.gitignore +6 -0
- arabam_mcp-0.1.0/LICENSE +21 -0
- arabam_mcp-0.1.0/PKG-INFO +85 -0
- arabam_mcp-0.1.0/README-fa.md +61 -0
- arabam_mcp-0.1.0/README-tr.md +61 -0
- arabam_mcp-0.1.0/README.md +73 -0
- arabam_mcp-0.1.0/pyproject.toml +22 -0
- arabam_mcp-0.1.0/server.json +1 -0
- arabam_mcp-0.1.0/src/arabam_mcp/__init__.py +3 -0
- arabam_mcp-0.1.0/src/arabam_mcp/http.py +117 -0
- arabam_mcp-0.1.0/src/arabam_mcp/listings.py +235 -0
- arabam_mcp-0.1.0/src/arabam_mcp/server.py +120 -0
- arabam_mcp-0.1.0/tests/__init__.py +0 -0
- arabam_mcp-0.1.0/tests/fixtures/detail_car.json +46 -0
- arabam_mcp-0.1.0/tests/fixtures/detail_price.html +3 -0
- arabam_mcp-0.1.0/tests/fixtures/detail_specs.html +34 -0
- arabam_mcp-0.1.0/tests/fixtures/facets_brands.json +55 -0
- arabam_mcp-0.1.0/tests/fixtures/facets_models.json +52 -0
- arabam_mcp-0.1.0/tests/fixtures/search_vehicles.json +89 -0
- arabam_mcp-0.1.0/tests/test_listings.py +157 -0
- arabam_mcp-0.1.0/tests/test_live.py +71 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
id-token: write # required for PyPI trusted publishing (OIDC)
|
|
12
|
+
contents: read
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: actions/setup-python@v5
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.12"
|
|
18
|
+
- run: pip install build
|
|
19
|
+
- run: python -m build
|
|
20
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
name: Publish to MCP Registry
|
|
2
|
+
|
|
3
|
+
# Official MCP Registry (registry.modelcontextprotocol.io) via GitHub OIDC.
|
|
4
|
+
# No secret, no interactive login: the runner's OIDC token proves ownership
|
|
5
|
+
# of the io.github.mohammad-abbasi-sh/* namespace.
|
|
6
|
+
# Refs: https://github.com/modelcontextprotocol/registry
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
tags: ["v*"]
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
publish:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
permissions:
|
|
17
|
+
id-token: write # required for OIDC -> MCP Registry
|
|
18
|
+
contents: read
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- name: Sync server.json version to tag
|
|
23
|
+
# Only sync when running on a version tag (v*). On workflow_dispatch
|
|
24
|
+
# (ref = refs/heads/main) keep the version committed in server.json.
|
|
25
|
+
run: |
|
|
26
|
+
VER=""
|
|
27
|
+
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
|
|
28
|
+
TAG="${GITHUB_REF#refs/tags/}"
|
|
29
|
+
VER="${TAG#v}"
|
|
30
|
+
fi
|
|
31
|
+
if [ -n "$VER" ]; then
|
|
32
|
+
python3 - "$VER" << 'EOF'
|
|
33
|
+
import json, sys
|
|
34
|
+
ver = sys.argv[1]
|
|
35
|
+
d = json.load(open('server.json'))
|
|
36
|
+
d['version'] = ver
|
|
37
|
+
for pkg in d.get('packages', []):
|
|
38
|
+
pkg['version'] = ver
|
|
39
|
+
json.dump(d, open('server.json', 'w'), indent=2, ensure_ascii=False)
|
|
40
|
+
print('server.json version ->', ver)
|
|
41
|
+
EOF
|
|
42
|
+
else
|
|
43
|
+
echo "No version tag ($GITHUB_REF); keeping server.json version as-is"
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
- name: Install mcp-publisher
|
|
47
|
+
run: |
|
|
48
|
+
curl -sL "https://github.com/modelcontextprotocol/registry/releases/download/v1.8.7/mcp-publisher_linux_amd64.tar.gz" -o /tmp/mcp-publisher.tar.gz
|
|
49
|
+
tar -xzf /tmp/mcp-publisher.tar.gz -C /tmp
|
|
50
|
+
sudo mv /tmp/mcp-publisher /usr/local/bin/mcp-publisher
|
|
51
|
+
mcp-publisher --version
|
|
52
|
+
|
|
53
|
+
- name: Validate server.json
|
|
54
|
+
run: mcp-publisher validate server.json
|
|
55
|
+
|
|
56
|
+
- name: Publish to MCP Registry (OIDC)
|
|
57
|
+
run: |
|
|
58
|
+
mcp-publisher login github-oidc
|
|
59
|
+
mcp-publisher publish server.json
|
|
60
|
+
# A registry outage must never break the release; re-run from Actions if needed.
|
|
61
|
+
continue-on-error: true
|
arabam_mcp-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mohammad Abbasi
|
|
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.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: arabam-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Unofficial read-only MCP server for arabam.com used-car listings (search, details, price stats)
|
|
5
|
+
Author: Mohammad Abbasi
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: httpx>=0.27
|
|
10
|
+
Requires-Dist: mcp<2
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# arabam-mcp 🇹🇷
|
|
14
|
+
|
|
15
|
+
mcp-name: io.github.mohammad-abbasi-sh/arabam-mcp
|
|
16
|
+
|
|
17
|
+
Unofficial **read-only** MCP server for **arabam.com**, Türkiye's used-car marketplace (~176k listings) — live search, full listing details with the 19-field spec table, brand/model browser, and market price stats.
|
|
18
|
+
|
|
19
|
+
> Built by Mohammad Abbasi as part of the "Turkey MCP factory" (Sepehr-style pipeline). Not affiliated with arabam.com.
|
|
20
|
+
|
|
21
|
+
## For AI agents
|
|
22
|
+
|
|
23
|
+
Add to your MCP client config (stdio):
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"mcpServers": {
|
|
28
|
+
"arabam": {
|
|
29
|
+
"command": "uvx",
|
|
30
|
+
"args": ["arabam-mcp"]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or run locally: `python -m arabam_mcp.server` (needs `mcp<2`, `httpx`).
|
|
37
|
+
|
|
38
|
+
## Tools
|
|
39
|
+
|
|
40
|
+
| Tool | Description |
|
|
41
|
+
|------|-------------|
|
|
42
|
+
| `arabam_search` | Search listings — `brand` slug ("bmw"), `model` slug ("bmw-3-serisi"), `page` 1..50 |
|
|
43
|
+
| `arabam_listing_detail` | Full detail of one listing (URL from search): price + 19-field spec table |
|
|
44
|
+
| `arabam_list_brands` | All ~77 brands with listing counts and URL slugs |
|
|
45
|
+
| `arabam_list_models` | Models of a brand (e.g. "bmw") with counts and slugs |
|
|
46
|
+
| `arabam_price_stats` | Market stats: min/max/median/average TRY price over N pages |
|
|
47
|
+
|
|
48
|
+
Example output (`arabam_search` / bmw 3-serisi):
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{"brand": "", "model": "bmw-3-serisi", "page": 1, "count": 20,
|
|
52
|
+
"listings": [{"id": "45288789", "title": "214 BİN KMDE SORUNSUZ A4 TDI",
|
|
53
|
+
"brand": "Audi", "year": 2019, "km": 214000, "price_try": 789900,
|
|
54
|
+
"url": "https://www.arabam.com/ilan/.../45288789"}]}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Example output (`arabam_listing_detail`):
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{"title": "Galeriden Honda Civic 1.4 S 1998 Model …", "price": 425000,
|
|
61
|
+
"price_currency": "TRY", "price_text": "425.000 TL",
|
|
62
|
+
"specs": {"Marka": "Honda", "Seri": "Civic", "Model": "1.4 S",
|
|
63
|
+
"Yıl": "1998", "Kilometre": "330.000 KM", "Vites tipi": "Düz",
|
|
64
|
+
"Yakıt tipi": "LPG & benzin", "Kimden": "Galeriden", …}}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Notes:
|
|
68
|
+
- Data comes from arabam.com's public server-rendered pages (schema.org Vehicle/Car JSON-LD + spec table) — no key, no login, read-only.
|
|
69
|
+
- Spec-table labels are Turkish (Marka, Seri, Model, Yıl, Kilometre, Vites tipi, Yakıt tipi, Kasa tipi, Renk, Motor hacmi, Motor gücü, Çekiş, Kimden…).
|
|
70
|
+
- Requests are spaced ~0.8s apart to stay polite.
|
|
71
|
+
- Environments behind an MITM proxy: set `SSL_CERT_FILE` to a CA bundle that trusts the proxy.
|
|
72
|
+
|
|
73
|
+
## Development
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
python3 -m venv .venv && .venv/bin/pip install "mcp<2" httpx pytest
|
|
77
|
+
.venv/bin/python -m pytest tests/ -q
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
- `tests/test_listings.py` — offline, parses real captured fixtures
|
|
81
|
+
- `tests/test_live.py` — 5 cautious live requests against arabam.com (needs `SSL_CERT_FILE` behind the lab proxy)
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
MIT
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# arabam-mcp 🇹🇷
|
|
2
|
+
|
|
3
|
+
سرور MCP غیررسمی و **فقطخواندنی** برای **arabam.com**، بازار خودروهای دستدوم ترکیه (حدود ۱۷۶ هزار آگهی) — جستوجوی زنده، جزئیات کامل هر آگهی با جدول ۱۹ فیلدی مشخصات، مرور برند/مدل و آمار قیمت بازار.
|
|
4
|
+
|
|
5
|
+
> ساختهٔ محمد عباسی در قالب «کارخانهٔ Turkey MCP» (خط تولید به سبک سپهر). وابستگی به arabam.com ندارد.
|
|
6
|
+
|
|
7
|
+
## برای ایجنتهای هوش مصنوعی
|
|
8
|
+
|
|
9
|
+
به پیکربندی کلاینت MCP خود (stdio) اضافه کنید:
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"mcpServers": {
|
|
14
|
+
"arabam": {
|
|
15
|
+
"command": "uvx",
|
|
16
|
+
"args": ["arabam-mcp"]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
یا بهصورت محلی اجرا کنید: `python -m arabam_mcp.server` (نیازمند `mcp<2` و `httpx`).
|
|
23
|
+
|
|
24
|
+
## ابزارها
|
|
25
|
+
|
|
26
|
+
| ابزار | توضیح |
|
|
27
|
+
|------|-------|
|
|
28
|
+
| `arabam_search` | جستوجوی آگهیها — `brand` مثل «bmw»، `model` مثل «bmw-3-serisi»، `page` از ۱ تا ۵۰ |
|
|
29
|
+
| `arabam_listing_detail` | جزئیات کامل یک آگهی (آدرس از خروجی جستوجو): قیمت + جدول ۱۹ فیلدی مشخصات |
|
|
30
|
+
| `arabam_list_brands` | همهٔ ~۷۷ برند با تعداد آگهی و نام کوتاه آدرس |
|
|
31
|
+
| `arabam_list_models` | مدلهای یک برند (مثلاً «bmw») با تعداد و نام کوتاه |
|
|
32
|
+
| `arabam_price_stats` | آمار بازار: کمینه/بیشینه/میانه/میانگین قیمت (لیر) روی N صفحه |
|
|
33
|
+
|
|
34
|
+
خروجی نمونه (`arabam_search` / بامو سری ۳):
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{"model": "bmw-3-serisi", "page": 1, "count": 20,
|
|
38
|
+
"listings": [{"id": "45288789", "title": "214 BİN KMDE SORUNSUZ A4 TDI",
|
|
39
|
+
"brand": "Audi", "year": 2019, "km": 214000, "price_try": 789900,
|
|
40
|
+
"url": "https://www.arabam.com/ilan/.../45288789"}]}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
نکتهها:
|
|
44
|
+
- دادهها از صفحات عمومی سمتسرور arabam.com میآیند (JSON-LD از نوع Vehicle/Car در schema.org + جدول مشخصات) — بدون کلید، بدون ورود، فقط خواندنی.
|
|
45
|
+
- برچسبهای جدول مشخصات ترکیاند (Marka، Seri، Model، Yıl، Kilometre، Vites tipi، Yakıt tipi، Kasa tipi، Renk، Motor hacmi، Motor gücü، Çekiş، Kimden…).
|
|
46
|
+
- بین درخواستها حدود ۰٫۸ ثانیه فاصله گذاشته میشود تا به سایت فشار نیاید.
|
|
47
|
+
- پشت پروکسی MITM: متغیر `SSL_CERT_FILE` را به بستهٔ CA مورد اعتماد پروکسی بدهید.
|
|
48
|
+
|
|
49
|
+
## توسعه
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
python3 -m venv .venv && .venv/bin/pip install "mcp<2" httpx pytest
|
|
53
|
+
.venv/bin/python -m pytest tests/ -q
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
- `tests/test_listings.py` — آفلاین، با نمونههای واقعی ذخیرهشده
|
|
57
|
+
- `tests/test_live.py` — ۵ درخواست محتاطانهٔ زنده به arabam.com
|
|
58
|
+
|
|
59
|
+
## مجوز
|
|
60
|
+
|
|
61
|
+
MIT
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# arabam-mcp 🇹🇷
|
|
2
|
+
|
|
3
|
+
Türkiye'nin ikinci el otomobil pazaryeri **arabam.com** için resmi olmayan, **salt okunur** MCP sunucusu — canlı ilan arama, 19 alanlık özellik tablosuyla tam ilan detayı, marka/model tarama ve piyasa fiyat istatistikleri.
|
|
4
|
+
|
|
5
|
+
> Mohammad Abbasi tarafından "Turkey MCP factory" (Sepehr tarzı hat) kapsamında geliştirildi. arabam.com ile bağlantısı yoktur.
|
|
6
|
+
|
|
7
|
+
## Yapay zekâ ajanları için
|
|
8
|
+
|
|
9
|
+
MCP istemci yapılandırmanıza (stdio) ekleyin:
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"mcpServers": {
|
|
14
|
+
"arabam": {
|
|
15
|
+
"command": "uvx",
|
|
16
|
+
"args": ["arabam-mcp"]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Veya yerelde çalıştırın: `python -m arabam_mcp.server` (`mcp<2`, `httpx` gerekir).
|
|
23
|
+
|
|
24
|
+
## Araçlar
|
|
25
|
+
|
|
26
|
+
| Araç | Açıklama |
|
|
27
|
+
|------|----------|
|
|
28
|
+
| `arabam_search` | İlan arama — `brand` ("bmw"), `model` ("bmw-3-serisi"), `page` 1..50 |
|
|
29
|
+
| `arabam_listing_detail` | Tek ilanın tam detayı (arama sonucundaki URL): fiyat + 19 alanlık özellik tablosu |
|
|
30
|
+
| `arabam_list_brands` | ~77 markanın tamamı, ilan sayıları ve URL kısaltmalarıyla |
|
|
31
|
+
| `arabam_list_models` | Bir markanın modelleri (örn. "bmw"), sayılar ve kısaltmalarla |
|
|
32
|
+
| `arabam_price_stats` | Piyasa istatistiği: N sayfadaki ilanların en düşük/en yüksek/ortanca/ortalama fiyatı (TL) |
|
|
33
|
+
|
|
34
|
+
Örnek çıktı (`arabam_search` / bmw 3-serisi):
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{"model": "bmw-3-serisi", "page": 1, "count": 20,
|
|
38
|
+
"listings": [{"id": "45288789", "title": "214 BİN KMDE SORUNSUZ A4 TDI",
|
|
39
|
+
"brand": "Audi", "year": 2019, "km": 214000, "price_try": 789900,
|
|
40
|
+
"url": "https://www.arabam.com/ilan/.../45288789"}]}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Notlar:
|
|
44
|
+
- Veriler arabam.com'un herkese açık sunucu taraflı sayfalarından gelir (schema.org Vehicle/Car JSON-LD + özellik tablosu) — anahtar yok, giriş yok, salt okunur.
|
|
45
|
+
- Özellik tablosu etiketleri Türkçedir (Marka, Seri, Model, Yıl, Kilometre, Vites tipi, Yakıt tipi, Kasa tipi, Renk, Motor hacmi, Motor gücü, Çekiş, Kimden…).
|
|
46
|
+
- İstekler arasında ~0,8 sn beklenir (nazik kullanım).
|
|
47
|
+
- MITM proxy arkasındaki ortamlar: `SSL_CERT_FILE` ile proxy'ye güvenen bir CA paketi gösterin.
|
|
48
|
+
|
|
49
|
+
## Geliştirme
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
python3 -m venv .venv && .venv/bin/pip install "mcp<2" httpx pytest
|
|
53
|
+
.venv/bin/python -m pytest tests/ -q
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
- `tests/test_listings.py` — çevrimdışı, gerçek yakalanmış örneklerle
|
|
57
|
+
- `tests/test_live.py` — arabam.com'a karşı 5 temkinli canlı istek
|
|
58
|
+
|
|
59
|
+
## Lisans
|
|
60
|
+
|
|
61
|
+
MIT
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# arabam-mcp 🇹🇷
|
|
2
|
+
|
|
3
|
+
mcp-name: io.github.mohammad-abbasi-sh/arabam-mcp
|
|
4
|
+
|
|
5
|
+
Unofficial **read-only** MCP server for **arabam.com**, Türkiye's used-car marketplace (~176k listings) — live search, full listing details with the 19-field spec table, brand/model browser, and market price stats.
|
|
6
|
+
|
|
7
|
+
> Built by Mohammad Abbasi as part of the "Turkey MCP factory" (Sepehr-style pipeline). Not affiliated with arabam.com.
|
|
8
|
+
|
|
9
|
+
## For AI agents
|
|
10
|
+
|
|
11
|
+
Add to your MCP client config (stdio):
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"mcpServers": {
|
|
16
|
+
"arabam": {
|
|
17
|
+
"command": "uvx",
|
|
18
|
+
"args": ["arabam-mcp"]
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or run locally: `python -m arabam_mcp.server` (needs `mcp<2`, `httpx`).
|
|
25
|
+
|
|
26
|
+
## Tools
|
|
27
|
+
|
|
28
|
+
| Tool | Description |
|
|
29
|
+
|------|-------------|
|
|
30
|
+
| `arabam_search` | Search listings — `brand` slug ("bmw"), `model` slug ("bmw-3-serisi"), `page` 1..50 |
|
|
31
|
+
| `arabam_listing_detail` | Full detail of one listing (URL from search): price + 19-field spec table |
|
|
32
|
+
| `arabam_list_brands` | All ~77 brands with listing counts and URL slugs |
|
|
33
|
+
| `arabam_list_models` | Models of a brand (e.g. "bmw") with counts and slugs |
|
|
34
|
+
| `arabam_price_stats` | Market stats: min/max/median/average TRY price over N pages |
|
|
35
|
+
|
|
36
|
+
Example output (`arabam_search` / bmw 3-serisi):
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{"brand": "", "model": "bmw-3-serisi", "page": 1, "count": 20,
|
|
40
|
+
"listings": [{"id": "45288789", "title": "214 BİN KMDE SORUNSUZ A4 TDI",
|
|
41
|
+
"brand": "Audi", "year": 2019, "km": 214000, "price_try": 789900,
|
|
42
|
+
"url": "https://www.arabam.com/ilan/.../45288789"}]}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Example output (`arabam_listing_detail`):
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{"title": "Galeriden Honda Civic 1.4 S 1998 Model …", "price": 425000,
|
|
49
|
+
"price_currency": "TRY", "price_text": "425.000 TL",
|
|
50
|
+
"specs": {"Marka": "Honda", "Seri": "Civic", "Model": "1.4 S",
|
|
51
|
+
"Yıl": "1998", "Kilometre": "330.000 KM", "Vites tipi": "Düz",
|
|
52
|
+
"Yakıt tipi": "LPG & benzin", "Kimden": "Galeriden", …}}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Notes:
|
|
56
|
+
- Data comes from arabam.com's public server-rendered pages (schema.org Vehicle/Car JSON-LD + spec table) — no key, no login, read-only.
|
|
57
|
+
- Spec-table labels are Turkish (Marka, Seri, Model, Yıl, Kilometre, Vites tipi, Yakıt tipi, Kasa tipi, Renk, Motor hacmi, Motor gücü, Çekiş, Kimden…).
|
|
58
|
+
- Requests are spaced ~0.8s apart to stay polite.
|
|
59
|
+
- Environments behind an MITM proxy: set `SSL_CERT_FILE` to a CA bundle that trusts the proxy.
|
|
60
|
+
|
|
61
|
+
## Development
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
python3 -m venv .venv && .venv/bin/pip install "mcp<2" httpx pytest
|
|
65
|
+
.venv/bin/python -m pytest tests/ -q
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- `tests/test_listings.py` — offline, parses real captured fixtures
|
|
69
|
+
- `tests/test_live.py` — 5 cautious live requests against arabam.com (needs `SSL_CERT_FILE` behind the lab proxy)
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
MIT
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "arabam-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Unofficial read-only MCP server for arabam.com used-car listings (search, details, price stats)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"mcp<2",
|
|
14
|
+
"httpx>=0.27",
|
|
15
|
+
]
|
|
16
|
+
authors = [{ name = "Mohammad Abbasi" }]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
arabam-mcp = "arabam_mcp.server:main"
|
|
20
|
+
|
|
21
|
+
[tool.hatch.build.targets.wheel]
|
|
22
|
+
packages = ["src/arabam_mcp"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"$schema":"https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json","description":"Unofficial read-only MCP server for arabam.com used-car listings: search, details, price stats.","name":"io.github.mohammad-abbasi-sh/arabam-mcp","packages":[{"identifier":"arabam-mcp","registryBaseUrl":"https://pypi.org","registryType":"pypi","transport":{"type":"stdio"},"version":"0.1.0"}],"repository":{"source":"github","url":"https://github.com/mohammad-abbasi-sh/arabam-mcp"},"title":"arabam.com Car Listings","version":"0.1.0","websiteUrl":"https://github.com/mohammad-abbasi-sh/arabam-mcp"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""HTTP layer for arabam_mcp — polite scraping of public SSR pages.
|
|
2
|
+
|
|
3
|
+
arabam.com serves fully server-rendered pages with no bot wall: listing data
|
|
4
|
+
comes from embedded JSON-LD (schema.org Vehicle/Car) and a facets JSON blob.
|
|
5
|
+
We keep requests gentle (one at a time, ~0.8s apart) and read-only.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
BASE = "https://www.arabam.com"
|
|
16
|
+
UA = (
|
|
17
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
|
18
|
+
"(KHTML, like Gecko) Chrome/120.0 Safari/537.36"
|
|
19
|
+
)
|
|
20
|
+
TIMEOUT = 25.0
|
|
21
|
+
MIN_INTERVAL = 0.8 # seconds between requests — be polite
|
|
22
|
+
|
|
23
|
+
_last_fetch = 0.0
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ArabamError(RuntimeError):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _client() -> httpx.Client:
|
|
31
|
+
# NOTE: proxy URL is read from the environment but never logged.
|
|
32
|
+
# trust_env=False because httpx fails to parse bracketed IPv6 entries
|
|
33
|
+
# (e.g. [::1]) that some environments put in no_proxy.
|
|
34
|
+
proxy = os.environ.get("https_proxy") or os.environ.get("HTTPS_PROXY")
|
|
35
|
+
# Standard SSL_CERT_FILE convention lets MITM-proxied environments
|
|
36
|
+
# point at a CA bundle that trusts the proxy; default uses certifi,
|
|
37
|
+
# which is correct for normal users.
|
|
38
|
+
verify: object = True
|
|
39
|
+
cafile = os.environ.get("SSL_CERT_FILE")
|
|
40
|
+
if cafile:
|
|
41
|
+
import ssl
|
|
42
|
+
|
|
43
|
+
verify = ssl.create_default_context(cafile=cafile)
|
|
44
|
+
return httpx.Client(
|
|
45
|
+
headers={"User-Agent": UA, "Accept-Language": "tr-TR,tr;q=0.9"},
|
|
46
|
+
timeout=TIMEOUT,
|
|
47
|
+
proxy=proxy or None,
|
|
48
|
+
verify=verify,
|
|
49
|
+
trust_env=False,
|
|
50
|
+
follow_redirects=True,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def search_url(brand: str | None, model: str | None, page: int) -> str:
|
|
55
|
+
slug_re = re.compile(r"^[a-z0-9][a-z0-9-]*$")
|
|
56
|
+
path = "ikinci-el/otomobil"
|
|
57
|
+
if model:
|
|
58
|
+
m = model.strip().lower()
|
|
59
|
+
if not slug_re.fullmatch(m):
|
|
60
|
+
raise ArabamError(f"bad model slug: {model!r}")
|
|
61
|
+
path += f"/{m}"
|
|
62
|
+
elif brand:
|
|
63
|
+
b = brand.strip().lower()
|
|
64
|
+
if not slug_re.fullmatch(b):
|
|
65
|
+
raise ArabamError(f"bad brand slug: {brand!r}")
|
|
66
|
+
path += f"/{b}"
|
|
67
|
+
url = f"{BASE}/{path}"
|
|
68
|
+
if page and page > 1:
|
|
69
|
+
url += f"?page={int(page)}"
|
|
70
|
+
return url
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def fetch(url: str) -> str:
|
|
74
|
+
"""GET a page; raises ArabamError on failure or bot-wall."""
|
|
75
|
+
global _last_fetch
|
|
76
|
+
wait = MIN_INTERVAL - (time.monotonic() - _last_fetch)
|
|
77
|
+
if wait > 0:
|
|
78
|
+
time.sleep(wait)
|
|
79
|
+
try:
|
|
80
|
+
with _client() as c:
|
|
81
|
+
r = c.get(url)
|
|
82
|
+
except httpx.HTTPError as e:
|
|
83
|
+
raise ArabamError(f"network error fetching {url}: {e}") from e
|
|
84
|
+
_last_fetch = time.monotonic()
|
|
85
|
+
if r.status_code != 200:
|
|
86
|
+
raise ArabamError(f"arabam.com returned HTTP {r.status_code} for {url}")
|
|
87
|
+
text = r.text
|
|
88
|
+
if 'name="description" content="px-captcha"' in text or "PerimeterX" in text:
|
|
89
|
+
raise ArabamError("arabam.com is showing a bot check for this request")
|
|
90
|
+
return text
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def fetch_search(brand: str | None = None, model: str | None = None,
|
|
94
|
+
page: int = 1) -> str:
|
|
95
|
+
if page is not None and (int(page) < 1 or int(page) > 50):
|
|
96
|
+
raise ArabamError("page must be between 1 and 50")
|
|
97
|
+
return fetch(search_url(brand, model, int(page) if page else 1))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def fetch_detail(url: str) -> str:
|
|
101
|
+
u = url.strip()
|
|
102
|
+
if u.startswith("/"):
|
|
103
|
+
u = BASE + u
|
|
104
|
+
if not re.fullmatch(r"https://www\.arabam\.com/ilan/[a-z0-9\-/]+", u):
|
|
105
|
+
raise ArabamError(
|
|
106
|
+
"detail URL must be an arabam.com /ilan/ listing URL "
|
|
107
|
+
"(get one from arabam_search first)"
|
|
108
|
+
)
|
|
109
|
+
return fetch(u)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def fetch_brands_page() -> str:
|
|
113
|
+
return fetch(f"{BASE}/ikinci-el/otomobil")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def fetch_brand_page(brand: str) -> str:
|
|
117
|
+
return fetch(search_url(brand, None, 1))
|